| 
      
      
      From: Ray Z. <rz...@co...> - 2002-01-04 21:59:52
      
     | 
| Chris,
I realized the other day that the enhanced has-a semantics I proposed 
in an e-mail to the list back on 7/3/01 is not quite general enough 
for all of our needs.  (I've included the entire original proposal at 
the end of this e-mail for reference).
The issue is that I assumed that any secondary objects auto-fetched 
(or lazy-fetched) with the primary object should always be auto-saved 
with that object as well. Based on this assumption, I did not include 
any explicit 'save' configuration in the new has-a syntax. The 3 
rules for saving were ...
----------
(I'm pulling this text directly out of the original write-up, where 
$x belongs to class X which has-a field named 'myA' which is an 
object of type A, and a field named 'myB' which is an object of type 
B) ...
1. 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.
2. 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.
3. 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.
----------
I've run across some cases where that isn't the desired behavior. In 
particular, sometimes one may not want to auto-save the secondary 
objects even though they are being auto/lazy-fetched. This could be 
the case for either forward or backward direction fetch 
configurations. On the other hand, one may want to allow auto-saving 
of linked secondary objects (not allowed by 3 above).
My new proposal is to let the old proposal define the default 
behavior, but add an optional parameter to the 'fetch' and 'link' 
parts of the configuration. In the 'fetch' configuration allow an 
optional 'no_save' parameter with values 0 or 1, where 0 corresponds 
to the default behavior (saving all secondary objects) and 1 
specifies that it should not do any saving of secondary objects.
In the 'link' configuration, allow an optional 'save' parameter with 
values 0 or 1, where 0 specifies the default (don't auto-save 
anything) and 1 means to save any linked objects, if necessary 
creating the corresponding linking objects.
There are more details to be worked out with this, but I thought I'd 
better send you what I have at the moment, before I get pulled off to 
other things.
	Ray
[ Original proposal from 7/3/01 (LONG) ]
------------------------------------------------------------------------
=================================
===    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
-- 
  Ray Zimmerman  / e-mail: rz...@co... / 428-B Phillips Hall
   Sr Research  /   phone: (607) 255-9645  /  Cornell University
    Associate  /      FAX: (815) 377-3932 /   Ithaca, NY  14853
 |