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 |