The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Object::POOF - Persistent Object Oriented Framework (for mod_perl)

VERSION

This document describes Object::POOF PLANS for version 0.0.7.

SYNOPSIS

Object::POOF doesn't try to work with every database. It's designed for use with MySQL's InnoDB transactional storage engine. Complex and invisible magic gives you a simplified way of creating objects that persist in the database. Where information can be deduced easily from looking at the database structure, POOF does not need you to tell it.

 package Myapp::DB;
 use base qw( Object::POOF::DB );
 # some config, see Object::POOF::DB manpage...

 package Myapp::Foo;            # table name is 'foo'
 use base qw( Object::POOF );
 use Readonly;
 Readonly our %relatives => (
    biz     => { class => 'Myapp::Biz' },
 );

 # ...some other class definitions for biz and its relationships...

 package main;

 my $db = Myapp::DB->new();
 my $foo = Myapp::Foo->new({ 
    db  => $db,                         # pass transact. thread
    pk  => { fooname => 'Mr. Foo' },    # where primary key = ...
 });

 print $foo->bar, "\n";         # table foo has column 'bar'

 # "s2o" == self-2-one
 # "s2m" == self-2-many
 # s2o / s2m ( 1:1 / 1:N ) relationships are determined by 
 # database schema, see 'Determining Relationships' below.
 # POOF doesn't need intermediary information to determine that.
 # Instead, it throws smart exceptions when something doesn't work.
 
 # but more than $foo->bar, stretch out through 
 # multiple relationship levels:

 # foo s2o biz s2m baz(id 42), column 'noz':

 print $foo->biz->baz({ pk => { id => 42 } })->noz, "\n";

 # all values are magically selected and populated 
 # with a single SELECT...LEFT JOIN query, and populated 
 # objects are cached in the Ranch.  Each s2m relation
 # called with parameters is answered by the Herd's Shepherd.
 # The Shepherd will be eventually offer up custom Herd 
 # data structures such as Tree::Simple (implementation goal).

 # so when you do this selection:
 
 my $jazz_master 
    = $foo
        ->biz
            ->baz({ pk => { id => 42 } })
                ->boz
                    ->diz({ 
                        pk => { 
                            first_name  => 'Dizzy', 
                            last_name   => 'Gillespe' 
                        } 
                    });
 
 # nothing in database is queried until 
 # following baz(id 42) s2o boz s2m diz at pk values.

 # if you had known you wanted a lot of objects 
 # pre-populated in advance:
 
 $foo->call({
    what => 'all',  # all property fields of a foo
    follow => {
        biz => {        # what not specified, defaults to 'all'
            follow => {
                baz => {    # again 'all' columns of baz
                    pk      => { id => 42 },
                    follow  => {
                        what    => [ 
                            qw( event_dt location_address ) 
                        ],
                        boz     => {
                            follow  => {
                                # get all diz's, incl. Dizzy:
                                diz     => { },  
                            },
                        },
                    },
                },
                food => {
                    pk      => { name => 'Corn Dog' },
                    what    => [ 'price' ],
                },
            },
        },
    },
 });   

But you have to be smart about how you use this kind of thing. For example, in the above call() statement, what if table baz has a big text column? If there are a lot of diz's in the boz of each baz, that text column of the intermediary relationship will be duplicated in the output of the LEFT JOIN, which could be a hit on the performance of the query and increase network volume. call() is a little bit greedy by default. You might have wanted to specify a 'what' part of the 'baz' hash to eliminate that big text field from the LEFT JOIN statement.

Specifying an order => [ [ column1 => 'DESC' ], [ column2 => 'ASC' ] ] parameter to parts of call() will eventually do what you expect. When referenced as an array, the herd will return with that order.

 # supposing 'location' is a column of table 'baz':

 foreach my $baz ( @{ $foo->biz->baz } ) {
    print $baz->location;  
 }

 # etc.

HUMOROUS DISCLAIMER

It doesn't work yet.

CURRENT WORK

Remove dependency on autoincrement primary key field named 'id.' Instead, can use one or more arbitrary fields that comprise the primary key. However, if the primary key consists of a single auto_increment field, it will be used like you expect. And if your package contains a function 'generate_primary_key', it will be used to generate a new one. Triggers are not implemented.

Call statement is optional to formulate a joined query; instead you can call out to a relationship and the Yodel will get all the data for you in a single statement.

Relatives are simplified, so you do not have to distinguish between one-to-many and one-to-one relationships. The type of relationship is a property of the database schema itself--- the way you define nested unique keys in the primary key fields. So, there is no need to duplicate this information in your packages. Instead, you just name a relationship to a class. If the foreign key is neither in this object's tables or in the remote object's tables, you have to specify the name of the intermediary relational table that contains the foreign key.

IN DEPTH

Determining relationships

Nested unique keys, foreign keys, primary key combinations and their positions in table structure determine 1:1 and 1:n relationships between entities. You should not have to duplicate that information in the class definition, or in some intermediary configuration file. Object::POOF::DB can analyze that information as it needs to.

So in the class definition, all it has to specify are the tables that belong to a class, and a hash of relatives that contain the class name and the table that contains the foreign key to the remote object.

So, rel hash format becomes:

 my $rels = {
    nickname => {
        class => 'MyApp::Whatsis',
        table => 'self_to_many_other',
    },

    nickname => {
        class => 'MyApp::Someother::Whatsis',
        table => 'self_to_one_other',
    },
 };

...where the tables are the table that contains the foreign key to the remote object.

So in a relationship from each Foo one-to-many Bar, tables would look like this:

 CREATE TABLE foo (
    fooname         VARCHAR(16) NOT NULL PRIMARY KEY,
    phone           VARCHAR(24)                         -- or whatever
 ) TYPE=InnoDB;

 INSERT INTO foo (fooname) VALUES 
    ('bob'), ('sally'), ('sue'), ('reynolds');

 CREATE TABLE bar (
    barname         VARCHAR(16) NOT NULL PRIMARY KEY,
    phone           VARCHAR(24)
 ) TYPE=InnoDB;

 INSERT INTO bar (barname) VALUES
    ('pig n boar'),                 ('cock n axe'),
    ('screaming jehosephat''s'),    ('the anaesthetic tide');


 CREATE TABLE haunt (
    fooname         VARCHAR(16) NOT NULL,

    barname         VARCHAR(16) NOT NULL,

    FOREIGN KEY (fooname) REFERENCES foo (fooname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    FOREIGN KEY (barname) REFERENCES bar (barname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    PRIMARY KEY (fooname, barname)
 ) TYPE=InnoDB;

Haunts don't need to be a separate class in their own right unless you need that relationship of Foos to Bars to have its own custom methods in a package. Otherwise, 'haunts' in this case can be read like a verb in the logic of your system. Each Foo haunts one or more Bar. You'd still say:

    foreach my $bar ( @{ $foo->haunt } ) { # see Foo class def below
        # ...
    }

Even if Haunt did have its own package, which related to Foos and to Bars, you still wouldn't need to change Foo's %relatives, unless a Foo needed access to the methods of your Haunt package. It is easiest to leave 'haunt' as simply a nickname for Bars in this type of relationship from Foo to Bar.

Our intrepid ship of foos needs work to support their drinking habits. So, they get jobs at the bars. But each person only has time to work at one bar, and each bar only needs to employ one person.

 CREATE TABLE barback (
    fooname         VARCHAR(16) NOT NULL UNIQUE,

    barname         VARCHAR(16) NOT NULL UNIQUE,

    FOREIGN KEY (fooname) REFERENCES foo (fooname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    FOREIGN KEY (barname) REFERENCES bar (barname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    PRIMARY KEY (fooname, barname)      
        -- nested unique keys enforce 1:1 rel
 ) Type=InnoDB;

Similarly, make only one of the primary keys have a nested unique key in order to determine a 1:Many relationship.

You can also group other fields in the table as nested unique keys to enforce other properties of your system. For example, the bars want to schedule attendance at events. They have arranged never to have events on the same day.

 CREATE TABLE foo_attending_bar_parties (
    party_dt        DATE NOT NULL UNIQUE,

    fooname         VARCHAR(16) NOT NULL,

    barname         VARCHAR(16) NOT NULL,

    FOREIGN KEY (fooname) REFERENCES foo (fooname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    FOREIGN KEY (barname) REFERENCES bar (barname)
        ON UPDATE CASCADE
        ON DELETE CASCADE,

    PRIMARY KEY (event_dt, barname)     -- nested unique key enforces
                                        -- only one party per day
 ) Type=InnoDB;

Then I can see about getting the exceptions to feed through a 'nice message' for display to the web browser through an Exception::Class method.

 package Drunken::Foo;
 use strict;
 use warnings;
 use base qw( Object::POOF );
 use Readonly;
 Readonly our %relatives => (
    haunt => {                              # interpreted as a 
        class => 'Drunken::Bar',            # a self-to-many
        table => 'haunt',
    },

    employer => {                           # self-to-one
        class => 'Drunken::Bar',
        table => 'barback',
    },

    party => {                              # one (self) to many
        class => 'Drunken::Bar',
        table => 'foo_attending_bar_parties',

        # access party date as $foo->party->party_dt...
        # party_dt from intermediary table will be mapped
        # from a secondary hash of the bar in this relationship 
        # to this foo and spat back automatically
    },
 );

Or, you may need to make parties more complicated, and entities in their own right, if you need methods in that frame of reference. So the party table would be:

 CREATE TABLE party (
    party_dt            DATE NOT NULL UNIQUE PRIMARY KEY,
    barname             VARCHAR(16),

    FOREIGN KEY (barname) REFERENCES bar (barname)
        ON UPDATE CASCADE ON DELETE CASCADE

 ) Type=InnoDB;

And you'd need to change the party schedule table:

 CREATE TABLE foo_attending_bar_parties (
    party_dt            DATE NOT NULL,
    fooname             VARCHAR(16) NOT NULL,

    FOREIGN KEY (party_dt) REFERENCES party (party_dt)
        ON UPDATE CASCADE ON DELETE CASCADE,

    FOREIGN KEY (fooname)  REFERENCES foo (fooname)
        ON UPDATE CASCADE ON DELETE CASCADE,

    PRIMARY KEY (party_dt, fooname)
 ) Type=InnoDB;

Then a Foo would be related 1:M to each Party they planned to go to.

 package Foo;
 # ...
 Readonly our %relatives => (
    bars_frequented => {                    # interpreted as 
        class => 'Drunken::Bar',            # a one-to-many
        table => 'haunts',
    },

    employer => {                           # one-to-one
        class => 'Drunken::Bar',
        table => 'barback',
    },

    party => {                              # one (self) to many
        class => 'Drunken::Party',
        table => 'foo_attending_bar_parties',
    },
 );

 package Party;
 # ...
 Readonly our %relatives => (
    bar => {                      # no table means one of our own
        class => 'Drunken::Bar',  # tables; in this case 'party'
    },                            # contains barname foreign key

    guests => {
        class => 'Drunken::Foo',
        table => 'foo_attending_bar_parties',
    },
 );

And somewhere in your code dealing with the party schedule, for a given foo and a given party, reference it like this to find the bar for the party on Halloween of 2005, if bob is scheduled to attend.

 my $halloween = 20051031;  # hopefully can make it work with DateTime

 my $bob = Foo->new({ db => $db, pk => { fooname => 'bob' } });

 my $barname = undef;
 eval {
    $barname = $bob
        ->party({ pk => { party_dt => $halloween } })
            ->bar
                ->barname;
 };
 if (!defined $barname) {
    $bob->add_party({ pk => { party_dt => $halloween } });
 }

In this case, the AUTOMETHOD as inherited by Foo will detect object wanted, so will push the call for 'party' to a Yodel.

What it pushes to the Yodel is more information than just a stack of strings. A stack of hashrefs that also has the parameters of any function called. Those parameters are interpreted in the case of pk => { party_dt => 20051031 } to infer that a foo relates one-to-many party, and we are asking for the party where the primary key is column party_dt with value 20051031. If, for example, there were no party on that date, or party_dt were not a primary key field, or some other error, Yodel would return an exception.

So first Want will be OBJECT. Is party already populated? No. Push { party => { pk => { party_dt => 20051031 } } } to call stack, return self.

Then Yodel will detect OBJECT wanted (bar).

If Yodel detects objects of class Bar in the Ranch pool, it could try to reduce the fields of bar selected in the query to just the primary key fields if there were a further select level, or just to the column barname instead of being greedy. Then the results of any fields obtained by the query will repopulate the entries in the ranch.

Because barname is a data field of this object, it's ready to yodel out for the objects. Assemble, execute, and parse the SELECT...JOIN call, passing values to Shepherds that populate Herds for each class and link member objects together.

Then return the bar object. When its AUTOMETHOD is called, it will find the field barname and return the contents, the string "screaming jehosephat's".

Assembling the primary key herd index for a multi-column primary key is going to be interesting. Maybe I should use a YAML dump of the pk hash (with all the keys/values) as a hash key. Hmmm.

Nicely, Yodel is the package that returns exceptions rethrown by the Object::POOF AUTOMETHOD. So they get spit back to the caller code, which can access the simple field, or stringify to get full message.

OLD SYNOPSIS

some of this doc may be wrong. this suite is still highly experimental and subject to change but i am releasing this version because i have a contract starting and i am always suspicious of corporations trying to lay claim to my work. - Mark Hedges 2005-09-06

 package MyApp::Foo;
 # a table named 'foo' exists in db with autoinc 'pk' field
 use Readonly;
 use base qw(Object::POOF);

 # make sure to make @more_tables and %relatives Readonly in 'our' scope.
 # (outside of the private block used by Class::Std)
 # this makes them accessible through the class without making an object.
 # (but they cannot be changed by anything but you, with a text editor.)
  
 # other tables containing fields to be joined 
 # to 'foo' by 'pk' field when fields are requested:
 Readonly our @more_tables => qw( foo_text foo_info foo_html );

 # relationships to other classes:
 Readonly our %relatives => (
     # self to other relationships (one to one):
     s2o => {
         # a table named 'bar' exists in db, and foo.bar_pk = bar.pk
         bar     => {
             class   => 'MyApp::Bar',
         },
     },

     # self contains many - 
     # each foo has only one rel to each sCm entity
     sCm => {
         # table named 'biz'; foo.pk = biz.foo_pk
         biz     => {
             class   => 'MyApp::Biz',
         },
     },
 
     # self to many - possibly more than one rel to each s2m entity,
     # uniquely enforced by a field of the interim relational table
     s2m => {
         baz     => {
             class       => 'MyApp::Baz',
             reltable    => 'r_foo_baz',
             relkey      => 'boz_pk',    # a 'boz' will be made here
         },
     },
 
     # many to many - possibly more than one rel to each m2m entity,
     # with no restrictions (reltable entry can be exactly duplicated)
     m2m => {
         boz__noz    => {
             class       => 'MyApp::Boz::Noz',
             reltable    => 'r_foo_boz__noz',
         },
     },
 );

 1; # end of package definition, unless you add custom object methods.

 ########

 # later in program...

 # a MyApp::DB thread inheriting from Object::POOF::DB
 my $db = MyApp::DB->new();


 # note: the transparent use of the future Yodel package to trick 
 # the -> operator will be able to skip over some inefficient 
 # laziness of selects and cache seen objects in the Ranch,
 # but if you know in advance what you want, you might as well tell
 # it to get the whole shebang at once, like this:

 my $foo = MyApp::Foo->new( {
    db      => $db,  
    where   => {
        # where a field of foo = some value
        fooprop => 'value',
    },
    follow  => {
        sCm => {
            biz => { 
                what => 'all',
            },
        },
        s2m => {
            baz => {
                what => [ qw( baz_prop1 baz_prop2 baz_prop3 ) ],
                follow => {
                    m2m => {
                        # MyApp::Baz m2m MyApp::Schnoz, follow it
                        schnoz => { 
                            what => [qw( schnoz_prop1 schnoz_prop2 )],
                        }, 
                    },
                },
            },
        },
    },
 });
 print "$foo->bar->barprop\n"; # s2m's are constructed by default

 # call a herd in array context (similar in arrayref context):
 foreach my $biz ($foo->bar->biz) {   
    $biz->somemethod( $foo->fooprop );  # or something
 }

 # call a herd in hash context (similar in hashref context):
 while (my ($baz_pk, $baz) = each $foo->baz) {
    print "adding rel for $baz_pk\n";
    $baz->add_rel( {
        m2m => {
            schnoz => [ $schnoz1, $schnoz2, $schnoz3, ],
        },
    });
    $baz->delete_rel( {
        m2m => {
            schnoz => $schnoz4,
        },
    });
 }

DESCRIPTION

Object::POOF provides a Persistent Object Oriented Framework implemented with some Perl Best Practices (as outlined in the O'Reilly book by that name.) It also provides a framework for applications running under mod_perl custom handlers (or, yuck, CGI scripts) that will handle de-tainting from easy form patterns, exporting of patterns to Javascript form validation, users and uri-based function permissions, and hopefully an accounting suite eventually.

For an OO application designer to get started, all they have to do is define the relationships of entity packages to other entities. Calls to various select and save routines are usually passed a hash reference similar in structure to the hash used to define relationships. Once you are into 'the zone' of writing with Object::POOF, you can stay in the mindset of the relationships between entities and (mostly) forget about writing SQL. For large group selects such as the one above using the 'follow' hashref, Object::POOF internals will format a huge join statement, parse it and populate all objects. It does not duplicate population of objects with the same pk, instead it keeps a 'Ranch' or pool of entities and then links them into the appropriate 'Herds' related to your point of view. The Object::POOF::Ranch can be used to do mass-selects of heavy data after following relations, and is intended to be an 'entity pool' to improve performance of mod_perl apps under Apache2 worker mpm. (This will require considerable thread-safe development.)

Then, you can call values of fields by identical accessor names and the next "herd" of related objects by accessors named the short names in the rel hashes. If you call an accessor that refers to valid fields or related entities that haven't been populated yet, they will be followed, so you can do 'lazy population.' But beware, with large groups of entities (like for a tabular report, for example), you'll have to use a follow call to get them all at once. But this can have its drawbacks too, since it uses a big left join that will make some data redundant. So, if there are big text fields or blobs or something in an intermediary relation of your depth of selection, and you think that will slow down the query, leave them out and then call a mass-select method for them:

 # following the above example, populate the many schnoz herds
 # with heavy text fields left out of the original follow query:
 my $ranch = $foo->get_ranch();
 $ranch->load( {
    class => 'MyApp::Schnoz',
    fields => [ qw( text1 text2 ) ],
 });

The Schnoz's will remain linked in the original related locations under your point of view from $foo->baz, but now for each Schnoz, $schnoz->text1 will not have to do a lazy select. The point is if you have 10,000 Schnoz's linked through in $foo, and you want to get the big text1 field from each of them, you don't want to slow down the original follow join with the field, but you don't want to have it do 10,000 queries either. The general rule is you have to think about what you're doing to make it most efficient.

INTERFACE

DIAGNOSTICS

Error message here, perhaps with %s placeholders

[Description of error here]

Another error message here

[Description of error here]

[Et cetera, et cetera]

CONFIGURATION AND ENVIRONMENT

Object::POOF requires no configuration files or environment variables.

DEPENDENCIES

Relies on use of transactional database. Currently only uses MySQL's InnoDB engine.

Object::POOF and its sub-modules require the following modules:

    Class::Std
    Class::Std::Utils
    Attribute::Types
    Contextual::Return
    Exception::Class
    Perl6::Export::Attrs
    Readonly
    List::MoreUtils
    Regexp::Common
    Data::Serializer
    YAML

INCOMPATIBILITIES

None reported. (Yeah right.)

BUGS AND LIMITATIONS

The LISP-ish excessive ending braces for call statements are annoying.

Because internals format a giant left join, it often decreases efficiency of SQL calls to select all information at once. Sticking to the pk fields and small data is a good idea, then call mass-select population methods in the ranch (see Object::POOF::Ranch).

The major bug as of this writing is that nothing actually works. :-D

Please report any bugs or feature requests to bug-object-poof@rt.cpan.org, or through the web interface at http://rt.cpan.org.

PLANS

At some point the Shepherd should start constructing custom Herd objects that implement more complex data structures based on key name fields like 'up_pk', 'child_pk', etc. that self-refer to objects of the same class/table.

SEE ALSO

Object::POOF::App(3pm), Object::POOF::DB(3pm), Object::POOF::Ranch(3pm), Object::POOF::Shepherd(3pm)

AUTHOR

Mark Hedges <hedges@ucsd.edu>

LICENCE AND COPYRIGHT

Copyright (c) 2005, Mark Hedges <hedges@ucsd.edu>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.

DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.