File Coverage

blib/lib/ObjectRowMap.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #!/bin/false
2              
3             # Copyright (c) 2002 Craig Welch
4             #
5             # You may distribute under the terms of either the GNU General Public
6             # License or the Artistic License, as specified in the Perl README file.
7              
8             package ObjectRowMap;
9              
10 1     1   4 use warnings;
  1         2  
  1         21  
11 1     1   5 use strict;
  1         2  
  1         36  
12              
13             our $VERSION = '0.11';
14              
15 1     1   1598 use DBI;
  0            
  0            
16              
17             my $ormapMeta;
18             if (!defined($ormapMeta)) {
19             $ormapMeta = {};
20             }
21              
22             END {
23             foreach my $k (keys(%{$ormapMeta})) {
24             if (exists($ormapMeta->{$k}{'dbh'})) {
25             my $dbh = $ormapMeta->{$k}{'dbh'};
26             $dbh->disconnect();
27             }
28             }
29             };
30              
31             sub new {
32             my $class = shift;
33             my $self = bless {},$class;
34             $self->init();
35             return $self;
36             }
37              
38             sub init {
39             my $self = shift;
40             my $r = ref($self);
41             my $sm = $self->ormapProperties($r);
42             $self->{'ormap'} = {'fieldsc'=>{}};
43              
44             if (!exists($sm->{'usePrepareCached'})) {
45             $self->{'ormap'}{'usePrepareCached'} = 0;
46             }
47             else {
48             $self->{'ormap'}{'usePrepareCached'} = $sm->{'usePrepareCached'};
49             }
50             if (!exists($sm->{'debug'})) {
51             $self->{'ormap'}{'debug'} = 0;
52             }
53             else {
54             $self->{'ormap'}{'debug'} = $sm->{'debug'};
55             }
56             if (!exists($sm->{'commitOnSave'})) {
57             $self->{'ormap'}{'commitOnSave'} = 1;
58             }
59             else {
60             $self->{'ormap'}{'commitOnSave'} = $sm->{'commitOnSave'};
61             }
62              
63             my $ormdbh;
64             if (exists($ormapMeta->{$r}{'dbh'})) {
65             $ormdbh = $ormapMeta->{$r}{'dbh'};
66             }
67              
68             if (exists($sm->{'dbh'}) and defined($sm->{'dbh'})) {
69             $self->{'ormap'}{'dbh'} = $sm->{'dbh'};
70             }
71             elsif (defined($ormdbh) and ($ormdbh->ping())) {
72             $self->{'ormap'}{'dbh'} = $ormapMeta->{$r}{'dbh'};
73             }
74             else {
75             $self->{'ormap'}{'dbh'} = DBI->connect(@{$sm->{'dbhConnectArgs'}});
76             $ormapMeta->{$r}{'dbh'} = $self->{'ormap'}{'dbh'};
77             }
78              
79             foreach my $k (sort(keys(%{$sm->{'persistFields'}}))) {
80             #if ($self->{'ormap'}{'debug'}) {
81             #print STDERR "ObjectRowMap:Debug:init - PersistField $k\n";
82             #}
83             $self->{'ormap'}{'fields'}{$k} = $sm->{'fields'}{$k};
84             push @{$self->{'ormap'}{'persistFields'}}, $k;
85             }
86             $self->{'ormap'}{'objIsNew'} = 1;
87             $self->{'ormap'}{'table'} = $sm->{'table'};
88             $self->{'ormap'}{'keyFields'} = $sm->{'keyFields'};
89             $self->clearChanged();
90             1;
91             }
92              
93             sub postSelectFieldString {
94             my $self = shift;
95             my @fields = @{$self->{'ormap'}{'persistFields'}};
96             return join(',',@fields);
97             }
98              
99             sub allAsList {
100             my $self = shift;
101             my $sql = "SELECT ".$self->postSelectFieldString()." FROM ".$self->{'ormap'}{'table'};
102             return $self->listFromQuery($sql);
103             }
104              
105             sub listFromQuery {
106             my $self = shift;
107             my $sql = shift;
108             my $r = ref($self);
109             my $dbh = $self->{'ormap'}{'dbh'};
110             my @fields = @{$self->{'ormap'}{'persistFields'}};
111             if ($self->{'ormap'}{'debug'}) {
112             print STDERR "ObjectRowMap:Debug:allAsList - SQL: $sql\n";
113             }
114             my $uda = $dbh->selectall_arrayref($sql);
115             my @toreturn;
116             for my $si (1..scalar(@{$uda})) {
117             my $i = $si - 1;
118             my @ud = @{$uda->[$i]};
119             my $hashload = {};
120             foreach my $k (0..$#ud) {
121             $hashload->{$fields[$k]} = $ud[$k];
122             }
123             my $newself = $r->new();
124             $newself->loadFromHash($hashload);
125             $newself->{'ormap'}{'objIsNew'} = 0;
126             push @toreturn, $newself;
127             }
128             return @toreturn;
129             }
130              
131             sub load {
132             my $self = shift;
133             my $dbh = $self->{'ormap'}{'dbh'};
134             my @fields = @{$self->{'ormap'}{'persistFields'}};
135             my $sql = "SELECT ".$self->postSelectFieldString()." FROM ".$self->{'ormap'}{'table'};
136             my @wheres = ();
137             foreach my $k (@{$self->{'ormap'}{'keyFields'}}) {
138             if (defined($self->{'ormap'}{'fields'}{$k})) {
139             push @wheres, " $k = '".$self->{'ormap'}{'fields'}{$k}."'";
140             }
141             }
142             if (scalar(@wheres) > 0) {
143             $sql .= " WHERE ".join(' and ', @wheres);
144             }
145             #because diff versions of dbi have diff versions of selectall_hashref...
146             if ($self->{'ormap'}{'debug'}) {
147             print STDERR "ObjectRowMap:Debug:load - SQL: $sql\n";
148             }
149             my $uda = $dbh->selectall_arrayref($sql);
150             my @ud = @{$uda->[0]};
151             my $hashload = {};
152             foreach my $k (0..$#ud) {
153             $hashload->{$fields[$k]} = $ud[$k];
154             }
155             $self->loadFromHash($hashload);
156             $self->{'ormap'}{'objIsNew'} = 0;
157             1;
158             }
159              
160             sub loadFromHash {
161             my $self = shift;
162             my $hashload = shift;
163             my $r = ref($self);
164             foreach my $k (keys(%{$hashload})) {
165             my $method = 'postLoad_'.$k;
166             if (defined($r->can("$method"))) {
167             $self->{'ormap'}{'fields'}{$k} = $self->$method($hashload->{$k});
168             }
169             else {
170             $self->{'ormap'}{'fields'}{$k} = $hashload->{$k};
171             }
172             }
173             $self->clearChanged();
174             1;
175             }
176              
177             sub save {
178             my $self = shift;
179             my $r = ref($self);
180             my @fields = @{$self->{'ormap'}{'persistFields'}};
181             my @keys = ();
182             my @qms = ();
183             my @vals = ();
184             my $sql = "";
185             foreach my $k (@fields) {
186             if (($self->{'ormap'}{'fieldsc'}{$k}) && (defined($self->{'ormap'}{'fields'}{$k}))) {
187             push @keys, $k;
188             push @qms, '?';
189             my $method = 'preSave_'.$k;
190             if (defined($r->can("$method"))) {
191             push @vals, $self->$method($self->{'ormap'}{'fields'}{$k});
192             }
193             else {
194             push @vals, $self->{'ormap'}{'fields'}{$k};
195             }
196             }
197             }
198             if (scalar(@keys) < 1) {
199             #nothing to save
200             return 1;
201             }
202             if ($self->{'ormap'}{'objIsNew'}) {
203             #insert syntax
204             $sql = "INSERT INTO ".$self->{'ormap'}{'table'}." (".join(',',@keys).") VALUES (".join(',',@qms).')';
205             }
206             else {
207             #update syntax
208             $sql = "UPDATE ".$self->{'ormap'}{'table'}." SET ";
209             foreach my $ki (0..$#keys) {
210             $sql .= " ".$keys[$ki]." = ?,";
211             }
212             chop $sql; #rm trailing ','
213             my @wheres = ();
214             foreach my $k (@{$self->{'ormap'}{'keyFields'}}) {
215             if (defined($self->{'ormap'}{'fields'}{$k}) and not ($self->{'ormap'}{'fieldsc'}{$k})) {
216             push @wheres, " $k = '".$self->{'ormap'}{'fields'}{$k}."'";
217             }
218             #else {
219             # print "Not update $k $self->{'ormap'}{'fieldsc'}{$k}\n";
220             #}
221             }
222             if (scalar(@wheres) > 0) {
223             $sql .= " WHERE ".join(' and ', @wheres);
224             }
225             else {
226             return 0; #we don't update if no key fields defined...
227             }
228             }
229             my $dbh = $self->{'ormap'}{'dbh'};
230             my $sth;
231             if ($self->{'ormap'}{'debug'}) {
232             print STDERR "ObjectRowMap:Debug:save - SQL: $sql\n";
233             }
234             if ($self->{'ormap'}{'usePrepareCached'}) {
235             $sth = $dbh->prepare_cached($sql);
236             }
237             else {
238             $sth = $dbh->prepare($sql);
239             }
240             #$res is rows affected
241             my $res = $sth->execute(@vals);
242             $sth->finish();
243             if ($self->{'ormap'}{'commitOnSave'}) {
244             $dbh->commit();
245             }
246             return $res;
247             }
248              
249             sub delete {
250             my $self = shift;
251             my @wheres = ();
252             my @fields = @{$self->{'ormap'}{'persistFields'}};
253             my $sql = "";
254             $sql = "DELETE FROM ".$self->{'ormap'}{'table'};
255             foreach my $k (@{$self->{'ormap'}{'keyFields'}}) {
256             if (defined($self->{'ormap'}{'fields'}{$k})) {
257             push @wheres, " $k = '".$self->{'ormap'}{'fields'}{$k}."'";
258             }
259             }
260             if (scalar(@wheres) > 0) {
261             $sql .= " WHERE ".join(' and ', @wheres);
262             }
263             else {
264             return 0; #we don't delete if no key fields defined...
265             }
266             my $dbh = $self->{'ormap'}{'dbh'};
267             my $sth;
268             if ($self->{'ormap'}{'debug'}) {
269             print STDERR "ObjectRowMap:Debug:delete - SQL: $sql\n";
270             }
271             $sth = $dbh->prepare($sql);
272             my $res = $sth->execute();
273             $sth->finish();
274             if ($self->{'ormap'}{'commitOnSave'}) {
275             $dbh->commit();
276             }
277             return $res;
278             }
279              
280             sub clearChanged {
281             my $self = shift;
282             foreach my $k (keys(%{$self->{'ormap'}{'fields'}})) {
283             $self->{'ormap'}{'fieldsc'}{$k} = 0;
284             }
285             1;
286             }
287              
288             sub get {
289             my $self = shift;
290             my $field = $_[0];
291             my $r = ref($self);
292             my $method = 'get_'.$field;
293             if (defined($r->can("$method"))) {
294             return $self->$method($self->{'ormap'}{'fields'}{$field});
295             }
296             else {
297             return $self->{'ormap'}{'fields'}{$field};
298             }
299             }
300              
301             sub set {
302             my $self = shift;
303             my $field = $_[0];
304             $self->{'ormap'}{'fieldsc'}{$field} = 1;
305             my $r = ref($self);
306             my $method = 'set_'.$field;
307             if (defined($r->can("$method"))) {
308             shift; #don't need the field name
309             $self->{'ormap'}{'fields'}{$field} = $self->$method(@_);
310             }
311             else {
312             $self->{'ormap'}{'fields'}{$field} = $_[1];
313             }
314             1;
315             }
316              
317             1;
318              
319             =head1 NAME
320              
321             ObjectRowMap - Simple perl object to DBI persistence engine
322              
323             =head1 DESCRIPTION
324              
325             ObjectRowMap is a Perl module which works with the DBI module to provide
326             a simple means to store a customized style of perl objects to anything with
327             a DBI module and generally SQL 92 (or later) syntax
328              
329             =head1 ObjectRowMap
330              
331             =begin docbook
332            
333            
334              
335             =end docbook
336              
337             =head1
338              
339             =head2 Version
340              
341             Version 0.11.
342              
343             =head2 Author and Contact Details
344              
345             The author is Craig Welch. He can be contacted via email to
346             Craig_Welch2 AT yahoo.com
347              
348              
349             =head2 Basic Usage
350              
351             ObjectRowMap must be inherited from to be of use, attempting to use it directly will not have the desired effect, whatever that might be. Create instances of your inheriting class.
352              
353             1. Required - Create a new class which uses and inherits from Object Row Map
354              
355             use vars qw( @ISA );
356             use ObjectRowMap;
357             push @ISA, 'ObjectRowMap';
358              
359             2. Required - Define a method called ormapProperties() in your new class to control the behaviour of ObjectRowMap
360              
361             There are a lot of clever things you could do here to handle connection pooling, obtaining database passwords, whatever is your pleasure. At the end of all that, you have to return a hash with the following (some portions are optional)
362              
363             Elements are flagged "req - required, reqor - required or, op - optional" in the list below: 'required' means just that, 'required or' means that it or the next (previous) is required (should be clear), 'optional' means just that (default provided)
364              
365             { 'table'=>'tablename','keyFields'=>['key','fields','(req)'],'usePrepareCached'=>'0 or 1, do I use prepare_cache instead of prepare, (op) (def 0)','dbhConnectArgs'=>['array ref of args to do dbh connection','(reqor)'],'dbh'=>'existing dbh (reqor)','persistFields'=>{'hash'=>'of','fields'=>'to','persist'=>'and','initial'=>'values,'(req)'=>''},'debug'=>1,'commitOnSave'=>1 }
366              
367             Simple Example:
368              
369             sub ormapProperties {
370             return { 'table'=>'ormtester','keyFields'=>['login','uid'],'dbhConnectArgs'=>["DBI:mysql:dbname=orm",'root','',{'AutoCommit'=>0}],'persistFields'=>{'login'=>'','uid'=>'','password'=>'','gecos'=>''},'debug'=>1,'commitOnSave'=>1};
371             }
372              
373             Some Explanation where it might be helpful-
374             dbh - if you are handling your own dbh, put it here and don't bother with dbhConnectArgs
375             keyFields - the fields which inidividually or together define a unique instance
376             dhbConnectArgs - if you don't handle your own dbh connection, you can just return exactly the arguments you would have sent to DBI::connect and it will do it for you. It maintains a single dbh per persistent class, in this case, do not define a dbh
377             persistFields - a hash of the fields you wish to persist and their initial values, you must also define key fields here. Instances of will automatically handle get and set for these (see below)
378             debug - if true, you'll see the sql which will be executed
379             commitOnSave - if true ObjectRowMap calls commit() at the end of save(). You can handle your own transactions if you passed in your own dbh. If you want to do this you will want to make this false. Otherwise it should be true or save will do you little good...
380              
381             3. External Requirements - setup your database (or other dbi source) and get the connection going. The column names that you care about must have the same names as your entries in 'persistFields'
382              
383             You are now done with the required components
384              
385             4. How it will work:
386              
387             Create a brand new never before seen instance and persist it:
388              
389             my $orm = new ObjectRowMap::Test();
390             $orm->set('login'=>'me');
391             $orm->set('gecos'=>'Myself');
392             $orm->set('uid'=>1);
393             $orm->set('password'=>'mypass');
394             print $orm->get('gecos')."\n";
395             $orm->save();
396              
397             Load an existing instance, and just get info from it:
398              
399             my $orm = new OrmTester();
400             $orm->set('login'=>'me');
401             $orm->load();
402             print $orm->get('gecos')."\n";
403              
404             Load an existing instance, change, and update it:
405              
406             my $orm = new OrmTester();
407             $orm->set('login'=>'me');
408             $orm->load();
409             $orm->set('gecos'=>'StillMe');
410             $orm->set('password'=>'mynewpass');
411             $orm->save();
412              
413             Load an existing instance and delete it:
414              
415             my $orm = new OrmTester();
416             $orm->set('login'=>'me');
417             $orm->load();
418             $orm->delete();
419              
420             =head2 Additional Explanation and some advanced topics
421              
422             All of these are object methods (including the ones which return multiple other objects - this is because of how ObjectRowMap handles things internally...) The only class method is 'new'
423              
424             load() - loads rest of object if "enough" values (e.g. key values) already set
425             save() - smart update of database from object, or inserts a new object (only changed fields)
426             loadFromHash() = for efficient loads from database with your own external query, mostely for internal use by allAsList and listFromQuery, no load from database will occur, all fields better be defined (very "raw")
427             allAsList() returns all instances of an object as an array (think about it, could be bad if you have a million records. May build a more sophistocated iteration based possiblity later. For working with "groups" of objects, see "listFromQuery")
428             listFromQuery() you provide the query, I provide the list of objects. The order and contents of the field part of the select are VERY important, you should use postSelectFieldString to get the query portion which follows the "SELECT" in your custom query
429             postSelectFieldString() - see listFromQuery above
430              
431             You can intercept a get or set for any field by defining YourPackage::get_fieldname() or YourPackage::set_fieldname(). This means your callers would still simply use $orm->set() and $orm->get() just like otherwise, but your "special" interceptor will be detected and called. The idea is that you can just drop in (or out) an interceptor without having to either change client code or define an accessor method for each field. How these work is a little asymmetric (like get and set themselves)-
432             $orm->set('field'=>'value') - YourPackage::set_fieldname() is called with ('value'), whatever you RETURN is stored in the correct place and success (1) is returned to the caller (you can do more storing of your own if you wish, of course, and ignore this - it's for convenience)
433             $orm->get('field') - YourPackage::get_fieldname() is called with the value which would have been stored by a previous set (whether you override set or not is immaterial) and what you RETURN is returned to the caller. Again, you are free to ignore this and just store things whever you want - since only $self->{'ormap'} is reserved, you have plenty of name space.
434              
435             pre-database processing for a field can be done by providing YourPackage::preSave_fieldname() (what you return is inserted instead of the actual field value, the actual field value is not modified in the process (unless you do it, of course)), YourPackage::postLoad_fieldname() will be called after a load with the raw database value (the field is not set before or after the call if you define this, it is up to you if you define it) - these are useful for pre and post processing related to special storage in database, things like encryption of values in the database, binary ip address storeage, date formats (some examples in ObjectRowMap::Test)
436              
437             You can use get and set for any non-persistant fields you desire and they are stored and saved the same as the persistent fields (the set_fieldname and get_fieldname interceptors will also work) - the only thing is that there is presently no automatic initialization during construction so you would have to do that yourself (and see the caveat below about overriding the default constructor) (and, of course, such fields are ignored during load and save, but then, that's what you want or you would make them persistent fields...)
438              
439             Of course, other than the methods discussed above and those below in Caveats, you can define your own methods. Actual values are not stored at the top of the self hash and where they are is an implementation detail which may change, so you should use $self->get() and $self->set() just like your callers
440              
441             A word about keys - an update will fail if at least one key for the object has not remained unchanged since the last load (for the where clause) (as it should... otherwise you would "miss" your instance's row and/or (if no keys defined) "hit" every row in the database (peek at how save builds the where clause if this doesn't make sense)). This also means that you cannot change all keys with a single load/set/save sequence - you will have to save between changing each key. You must define enough of your keys to achieve uniqueness before a load, otherwise you just get the first row returned. It supports multiple keys but has no idea how many are required for uniqueness, it will use all that it has which are defined (you can initialize them to undef to make full use of this behaviour)...
442              
443             =head2 DBI/DBD/Database compatibility
444              
445             Uses a minimal portion of SQL 92, should work with practically any DBD module which correctly implements 'ping', it's been tested with mysql and postgress, but I can't imagine why it would not work with nearly anything
446              
447             =head2 Caveats and Limitations
448              
449             You must call $self->ObjectRowMap::init() at construction if you override default constructor. At present the contructor ignores anything you may pass to it.
450             $self->{'ormap'} is a reserved element of the self hash
451             YourClass->ormapMeta{} is a reserved class level variable
452              
453             set and get only work for one field and field/value respectively. This is to keep things as relatively clean and efficient as they are
454              
455             don't define methods for load, save, loadFromHash, allAsList, listFromQuery or clearChanged unless you have read the code and know what you are doing
456              
457             Instances are not thread synchronized and if you simply provide connection strings
458             for ObjectRowMap to create database handles, it will only use one per class. This means
459             that in a multi-threaded implementation there could be a problem with multiple threads
460             using the same dbh at the same time. If you synchronize access by class or handle your database handles yourself in a threadsafe way you should be OK. It would not be hard to make it threadsafe by default, I may at some point. If you are really paranoid, you would want a semaphore per object and override/lock for all "set's", if you are just smartly-cautious all you have to worry about is synchronization of database handles.
461              
462             It's really simple. This is a caveat and a limitation. It's been extremely useful to me but since it doesn't handle multiple-table objects or anything but 1:1 relationships (automatically, that is, you could do some things on your own within it's framework to accomplish that...), it will require more work on your part to handle these more complex things. I wrote it in a few hours after having re-written similar custom functionality over and over on a project and found it was just fine for my needs so I never extended it - feel free, or you can ask for specific additions or my thoughts on them - I've got a plan for more complex objects, but haven't written code for it.
463              
464             =cut
465              
466             1;