From an e-mail to the Openinteract Dev list ...
<http://www.geocrawler.com/archives/3/8393/2001/7/0/6095774/>

From:    Ray Zimmerman <rz10@cornell.edu>
Subject: SPOPS enhanced HAS-A semantics (long)
Date:    July 3, 2001 11:12:44 AM EDT
To:      openinteract-dev@lists.sourceforge.net

Hi Chris and others,

As I mentioned in an earlier e-mail to this list, we're looking into 
using SPOPS to implement a generalized Perl object persistence 
framework which can automatically handle inheritance and cascading 
fetches/saves/removes, etc.

For example, from the model included with my earlier e-mail, we may 
want to think of an Anchor object as being an attribute of a Boat 
object, where fetching, saving or removing a Boat object 
automatically fetches, saves or removes the corresponding Anchor 
object.

This sort of functionality can be implemented on a case by case basis 
by defining the appropriate (pre|post)_(save|fetch|remove)_action 
methods in the Boat class. However, if this functionality is needed 
for many classes there would be a lot of duplication of code. To get 
around this I've worked on the definition of a new has-a 
configuration syntax which would allow one to specify the desired 
cascading behavior in the configuration for the class.

This new syntax also would replace the current SPOPS 'links_to' 
syntax. It does this by making the rows of the linking table into 
just another SPOPS object with two 'has_a' fields which link one 
object to another. This is a bit more general than the current 
'links_to' since it allows one to add other attributes to the linking 
object. E.g. If a ClubMembership is what links a Person and a Club, 
the ClubMembership could also have an effective date and expiration 
date.

So, I would like to propose the following as a starting point for an 
enhanced 'has_a' functionality for SPOPS.

<snipped a few questions to Chris Winters>

  Ray Zimmerman  / e-mail: <EMAIL: PROTECTED> / 428-B Phillips Hall
   Sr Research  /   phone: (607) 255-9645  /  Cornell University
    Associate  /      FAX: (815) 377-3932 /   Ithaca, NY  14853



=================================
===    New Has-A Semantics    ===
=================================

Assuming ...

    X has-a A, and
    X has-a B (that is, X links A to B)

We want to allow the 'has_a' configuration for 'X has-a A' to define the following options for ...

... fetch behavior:
    (1) all manual fetches & saves (default)
    (2) fetch X auto-fetches A & save X auto-saves A
    (3) fetch X lazy-fetches A & save X auto-saves A
    (4) fetch A auto-fetches X's into $a->{'list_of_Xs'} & save A auto-saves X's
    (5) fetch A lazy-fetches X's into $a->{'list_of_Xs'} & save A auto-saves X's
    (6) fetch A auto-fetches B's into $a->{'linked_Bs'} (no auto-saving)
    (7) fetch A lazy-fetches B's into $a->{'linked_Bs'} (no auto-saving)

... remove behavior:
    (1) all manual removes (default)
    (2) remove X auto-removes A
    (3) remove A auto-sets to NULL has_a field in X's
    (4) remove A auto-removes X's
        (if 'X has_a B' is configured to auto-remove B, then
         removing A will auto-remove corresponding X's and B's)


======================
 Configuration Syntax
======================

$CONF = {
    X_alias => {
        class       => 'X',
        field       => [ qw/ x_id x_data myA myB / ],
        has_a       => {    
            myA =>  {
                class       => 'A',
                fetch       => {
                    type    =>  'manual|auto|lazy|manual_by|auto_by|lazy_by',
                    name    => 'someA',
                    list_field  =>  'list_of_Xs',
                },
                link        => {
                    type    =>  'manual|auto|lazy',
                    field   =>  'myB'
                    name    =>  'someB',
                    list_field  =>  'linked_Bs',
                },
                remove      => {
                    type    =>
                        'manual|auto|manual_by|auto_by|manual_null|auto_null'
                    name    =>  'removeSomeA',
                    list_field  =>  'list_of_Xs',
                }
            },
            myB =>  {
                class       => 'B',
                fetch       => {
                    type    =>  'manual|auto|lazy|auto_by|lazy_by',
                    name    => 'someB',
                    list_field  =>  'linked_Bs',
                },
                link        => {
                    type    =>  'manual|auto|lazy',
                    field   =>  'myA'
                    name    =>  'someA',
                    list_field  =>  'list_of_As',
                },
                remove      => {
                    type    =>
                        'manual|auto|manual_by|auto_by|manual_null|auto_null'
                    name    =>  'removeSomeB',
                    list_field  =>  'list_of_Xs',
                }
            },
        }
    },
    A_alias => {
        class       => 'A',
        field       => [ qw/ a_id a_data / ],
        list_field  => { X => ['list_of_Xs'],
                         B => ['linked_Bs'] }
    },
    B_alias => {
        class       => 'B',
        field       => [ qw/ b_id b_data / ],
    },
}


==========
 Overview
==========

For the sake of clarity in the discussion, assume that X has-a A and X has-a B, namely fields myA and myB, respectively, and we're examining the meaning of the has_a data corresponding to the myA field.

The new 'has_a' config data is a hashref where the keys are the names of fields in the table containing IDs of other objects (foreign keys). These can be names of fields in inherited classes with certain restrictions. Each value in the hash is another hashref with the following possible keys ...

    class
    fetch
    link
    remove

The value for 'class' is the name of the class of objects being linked by this field. If this field is defined in a parent class (as opposed to the current class) then the class specified here must be a sub-class of the one specified in the has_a config of the parent class. (E.g. if a Team has-a Coach, it's fine to override the has_a specification for the coach field in Team to say that SoccerTeam has-a SoccerCoach, but not that SoccerTeam has-a TeamCaptain.)

The values for 'fetch', 'remove' and 'link' are each hashrefs, with possible keys as follows:

    fetch       remove      link
    -----       ------      ----
    name        name        name
    type        type        type
    list_field  list_field  list_field
                            field

The 'fetch' parameters specify what methods will be automatically created for fetching one object from the other (methods in X to fetch the A or in A to fetch the X's) and when they are called.

The 'remove' parameters specify what methods will be automatically created for removing one object from the other (methods in X to remove the A or in A to remove the X's) and when they are called.

If the 'link' key is present it means that this class (X) serves to link two other objects together via this field (myA) and another has-a field (myB). The 'link' parameters specify what methods will be automatically created in the foreign object (A) for adding and removing links (instances of X) to the other object (B) and for accessing the linked objects, and when these methods are called.


=======
 Fetch
=======

The fetch specification allows for 3 types of fetch operations for the has-a relationship in question, manual, automatic and lazy, either in the forward direction (given an X, fetch its A) or in the reverse direction (given an A, fetch its list of X's). The types 'manual', 'auto' and 'lazy' indicate a forward direction and 'manual_by', 'auto_by' and 'lazy_by' indicate a reverse direction (that the corresponding X's will be fetched BY an A). In the case of the forward direction, we'll call X the primary object and A the secondary object. For the reverse direction, A will be primary and X secondary. Using that terminology, manual means that secondary object are only fetched in response to an explicit request to fetch them. Auto means they are fetched automatically when the primary object is fetched (and they are stashed in the primary object), and lazy means they are fetched (and stashed) by the primary object only at the moment an access to the secondary object is attempted.

The methods that are created are always created in the primary object for fetching the secondary object(s).

The forward direction specifies that a method be created in X for fetching the corresponding A. If the 'name' parameter is present, it gives the name of the method to create. If it is not present, it will use the default name 'fetch_<field_name>' (in this example 'fetch_myA'). This method accepts only a hashref of parameters (db handle, etc) as input args and returns the secondary object.

The reverse direction specifies that a method be created in A for fetching the list of corresponding X's. If the 'name' parameter is present, it gives the name of the method to create, otherwise it will use the default name 'fetch_<list_field>' (in this example 'fetch_list_of_Xs'). This method accepts only a hashref of parameters (db handle, etc) as input args and returns an arrayref of the secondary objects.

The 'list_field' parameter, which is mandatory for reverse direction, must match a list_field defined for this class in the primary object's configuration (i.e. the list_field specification in class A's config).

If the fetch type is set to 'auto', assuming default naming of methods, then doing ...

    $x = X->fetch($x_id);

... automatically does ...

    $x->{myA} = $x->fetch_myA;

... during the post_fetch stage. If the fetch type is set to 'lazy', the second operation happens whenever $x->{myA} is accessed the first time. In all forward direction fetch configurations (manual, auto and lazy), if $x->{myA} contains a reference to an A object when $x is saved, it will save the A object during the pre_save stage before saving $x with the updated id of the A object. If $x->{myA} contains an id only, nothing special is done when $x is saved.

If the fetch type is set to 'auto_by', assuming default method naming, then doing ...

    $a = A->fetch($a_id);

... automatically does ...

    $a->{'list_of_Xs'} = $a->fetch_list_of_Xs;

... during the post_fetch stage. If the fetch type is set to 'lazy_by', the second operation happens whenever $a->{'list_of_Xs'} is accessed the first time. In all reverse direction fetch configurations (manual_by, auto_by and lazy_by), if $a->{'list_of_Xs'} contains an arrayref of X objects when $a is saved, it will save each X during the post_save stage after updating the myA field in each X with the new id of $a.

For auto_by and lazy_by, two additional methods are created in A, one for adding objects to its list of X's and one for removing objects from it. These can only be used after the A object has been saved. Their primary purpose is to keep the list in memory in sync with what's in the database, so when using auto_by or lazy_by it's a good idea to use only these methods to add or remove corresponding X's. If the 'name' parameter is present, the methods are named add_<name> and remove_<name>. If the 'name' parameter is not present they are named add_to_<list_field> and remove_from_<list_field>. The method to add X's takes an X object or an arrayref of X objects as inputs and returns the same object or arrayref to the objects after saving them. The method to remove X's takes an id or arrayref of ids and returns the number of X's successfuly removed.


========
 Remove
========

The remove specification allows for 6 types of remove operations for the has-a relationship in question, 'manual', 'auto', 'manual_by', 'auto_by', 'manual_null' and 'auto_null'. The types 'manual' and 'auto' specify a forward direction (given an X, remove its A) and 'manual_by' and 'auto_by' specify a reverse direction (given an A, remove it's list of X's). The types 'manual_null' and 'auto_null' also specify a reverse direction, but instead of removing the list of X's corresponding to a given A, they set the linking field (myA, in the example) to NULL in each of the X's.

For the forward direction ('manual' and 'auto'), a method is created in X to remove the corresponding A. If the 'name' parameter is present, it gives the name of the method to create. If it is not present, it will use the default name remove_<field_name> (in this example 'remove_myA'). This method accepts only a hashref of parameters (db handle, etc) as input args and returns a true value if the remove was successful. If the type is 'auto', this method will be called automatically in the pre-remove phase whenever X is removed. That is ...

    $x->remove;

... automatically does ...

    $x->remove_myA;

... during the pre_remove stage.

The reverse direction ('manual_by' and 'auto_by') specifies that a method be created in A for removing the corresponding X's. If the 'name' parameter is present, it gives the name of the method to create, otherwise it will use the default name 'remove_<list_field>' (in this example 'remove_list_of_Xs'). This method accepts only a hashref of parameters (db handle, etc) as input args and returns the number of objects removed.

The 'list_field' parameter, which is mandatory for reverse direction, must match a list_field defined for this class in the primary object's configuration (i.e. the list_field specification in class A's config).

If the remove type is set to 'auto_by', assuming default method naming, then doing ...

    $a->remove;

... automatically does ...

    $a->remove_list_of_Xs;

... during the pre_remove stage.

If the remove type is set to 'manual_null' or 'auto_null' a method is created in A for setting the field in each of the X's corresponding to that A to NULL. If the 'name' parameter is present, it gives the name of the method to create, otherwise it will use the default name 'null_<list_field>' (in this example 'null_list_of_Xs').

If the remove type is 'auto_null', assuming default method naming, then doing ...

    $a->remove;

... automatically does ...

    $a->null_list_of_Xs;

... during the pre_remove stage.
    

======
 Link
======

The link specification, is used to indicate that the class (X) is used to link two objects together via this field (myA) and another has-a field. The 'field' parameter specifies the name of the other has-a field (myB in this example). A method will be created in A to allow an object of type A to fetch all of the objects of type B which are linked to that A via the corresponding X's. If the 'name' parameter is present, it gives the name of the method to create, otherwise it will use the default name 'fetch_<list_field>' (in this example 'fetch_linked_Bs'). This method accepts only a hashref of parameters (db handle, etc) as input args and returns an arrayref of the linked objects.

There are two other methods which are defined which allow an A to easily create and remove links (objects of class X) to B's. If the 'name' parameter is present, they will be called 'add_link_<name>' and 'remove_link_<name>', otherwise the default names, 'add_link_<list_field>' and 'remove_link_<list_field>', are used. Both methods take an id or an arrayref of ids of B objects as inputs and return the number of links added or removed respectively.

If the link type is set to 'auto', assuming default method naming, then doing ...

    $a = A->fetch($a_id);

... automatically does ...

    $a->{'linked_Bs'} = $a->fetch_linked_Bs;

... during the post_fetch stage. If the link type is set to 'lazy', the second operation happens whenever $a->{'linked_Bs'} is accessed the first time.

For all links (manual, auto and lazy), whether or not $a->{'linked_Bs'} contains anything when $a is saved, it will NOT automatically save any X's or B's.


============
 Misc Notes
============

- If X links A and B, then when adding a link via $a->add_link_linked_Bs(), how would other fields in X (besides myA and myB) be specified?

- Maybe the add_link_<link_field> and remove_link_<link_field> methods are unecessarily asking for trouble and we should only allow links (X objects) to be created and removed directly via methods in X (not via A). These methods were an attempt to duplicate the current SPOPS links_to functionality by making the linking table correspond to just another SPOPS object. This also allows for a bit more generality in that the linking table can also add other attributes to the linking table (e.g. an effective date on a club membership).

- We must avoid has_a cycles (e.g. Boat has_a anchor and Anchor has_a Boat, both set up to auto-remove/fetch the other).


==========
 Examples
==========

All Manual
----------
    $CONF = {
        X_alias => {
            class   => 'X',
            field   => [ qw/ x_id myA / ],
            has_a   => {
                myA     => {
                    class   =>  'A',
                    fetch   =>  { type  => 'manual' },
                    remove  =>  { type  => 'manual' }
                }
            }
        },
        A_alias => {
            class   => 'A',
            field   => [ qw/ a_id a_data / ]
        },
    };

    $x    = X->new              # create a new X
    $a    = A->new              # create its A
    $a_id = $a->save;           # save the A
    $x->{myA} = $a_id;          # put the A's id in the X
    $x_id = $x->save;           # save the X
    $x    = X->fetch($x_id);    # fetch an X
    $a_id = $x->myA;            # get the id of its A
    $a    = $x->fetch_myA;      # fetch the A object
    $a->remove;                 # remove the A
    $x->remove;                 # remove the X



Auto | Lazy (think X=Boat has-a A=Anchor)
-----------
    $CONF = {
        X_alias => {
            class   => 'X',
            field   => [ qw/ x_id myA / ],
            has_a   => {
                myA     => {
                    class   =>  'A',
                    fetch   =>  { type => 'auto' }, # or 'lazy'
                    remove  =>  { type => 'auto' }
                }
            }
        },
        A_alias => {
            class   => 'A',
            field   => [ qw/ a_id a_data / ]
        },
    };

    $x    = X->new({myA => A->new});    # create a new X and its new A
    $x_id = $x->save;           # save the X and its A
    $x    = X->fetch($x_id);    # fetch an X and its A
    $a    = $x->myA;            # get the A out of the X
    $x->remove;                 # remove the X and its A
    


Auto | Lazy By  (think X=Slip has-a A=Boatyard)
--------------
    $CONF = {
        X_alias => {
            class   => 'X',
            field   => [ qw/ x_id myA / ],
            has_a   => {
                myA     => {
                    class   =>  'A',
                    fetch   =>  { type          => 'auto_by',   # or 'lazy_by'
                                  list_field    => 'list_of_Xs'
                                },
                    remove  =>  { type => 'auto_by' }
                }
            }
        },
        A_alias => {
            class       => 'A',
            field       => [ qw/ a_id a_data / ]
            list_field  => { X => ['list_of_Xs']    }
        },
    };

    $a    = A->new;             # create new A
    $a->add_to_list_of_Xs( [X->new, X->new] );  # add some new X's
    $a_id = $a->save;           # save the A and all of its X's
    $a    = A->fetch($a_id);    # fetch the A and all of its X's
    $x1 = $a->{list_of_Xs}->[1];# access a particular X
    $a->remove;                 # remove the A and all of its X's
    

Auto | Lazy Link    (think X=BoatyardSlipLink has-a A=Boatyard, B=Slip)
----------------
    $CONF = {
        X_alias => {
            class   => 'X',
            field   => [ qw/ x_id myA myB / ],
            has_a   => {
                myA     => {
                    class   =>  'A',
                    link    =>  { type  => 'auto',
                                  field => 'myB',
                                  list_field => 'linked_Bs',
                                },
                    remove  =>  { type => 'auto_by' }
                },
                myB     => {
                    class   =>  'B',
                    fetch   =>  { type => 'lazy' },
                    remove  =>  { type => 'auto' }
                }
            }
        },
        A_alias => {
            class       => 'A',
            field       => [ qw/ a_id a_data / ]
            list_field  => { X => ['linked_Bs'] }
        },
        B_alias => {
            class       => 'B',
            field       => [ qw/ b_id b_data / ]
        },
    };

    $a    = A->new;             # create new A
    $a_id = $a->save            # save the A
    $b1_id = B->new->save;      # create and save some B's
    $b2_id = B->new->save;
    $b3_id = B->new->save;
    $a->add_link_linked_Bs($b1_id); # create & save X which links $a to $b1
    $a->add_link_linked_Bs([$b2_id,$b3_id]);    # and $b2 and $b3
    $a->remove_link_linked_Bs($b2_id);  # remove X which links $a to $b2
                                        # (the X also auto-removes $b2)
    $a    = A->fetch($a_id);    # fetch the A and all of its linked B's
    $a->remove;                 # remove the A and all of its X's (& linked B's)
    

Manual Link (think X=ClubMembership has-a A=Club, B=Person)
-----------
    $CONF = {
        X_alias => {
            class   => 'X',
            field   => [ qw/ x_id myA myB / ],
            fetch_by=> [ qw/ myA myB /]
            has_a   => {
                myA     => {
                    class   =>  'A',
                    link    =>  { type  => 'manual',
                                  field => 'myB',
                                  list_field => 'linked_Bs',
                                },
                    remove  =>  { type => 'auto_by' }
                },
                myB     => {
                    class   =>  'B',
                    link    =>  { type  => 'manual',
                                  field => 'myA',
                                  list_field => 'linked_As',
                                },
                    remove  =>  { type => 'auto_null' }
                }
            }
        },
        A_alias => {
            class       => 'A',
            field       => [ qw/ a_id a_data / ]
            list_field  =>  'linked_Bs',
        },
        B_alias => {
            class       => 'B',
            field       => [ qw/ b_id b_data / ]
            list_field  =>  'linked_As',
        },
    };

    $a1   = A->new;             # create some new A's ...
    $a2   = A->new;
    $b1   = B->new;             # ... and some new B's ...
    $b2   = B->new;
    $b3   = B->new;
    $a1_id = $a1->save;         # ... and save them
    $a2_id = $a2->save;
    $b1_id = $b1->save;
    $b2_id = $b2->save;
    $b3_id = $b3->save;
    $a1->add_link_linked_Bs([$b1_id, $b2_id]);  # create some linking X's via A
    $b3->add_link_linked_As([$a1_id, $a2_id]);  # create some linking X's via B
    $x4 = X->new({myA=>$a2_id,myB=>$b1_id})->save;  # create linking X directly
    [$b1, $b2, $b3] = $a1->fetch_linked_Bs;     # fetch B's linked to a given A
    [$x1, $x2, $x3] = X->fetch_by_myA($a1_id);  # fetch X's for a given A
    $a2->remove;    # remove an A and it's corresponding X's (but not the B's)
    $a1->remove_link_linked_Bs([$b1_id]);       # remove linking X
    $b2->remove;            # remove a B and set field in linking X's to NULL
