|
NAMESPOPS::Manual::Relationships - SPOPS object relationshipsSYNOPSISThis document aims to answer the following questions:
DESCRIPTIONObjects are great by themselves, but some real power comes when you can declaratively relate objects to one another. SPOPS allows you to do this through the class configuration.The two types of relationships are called 'has_a' and 'links_to'. The 'has_a' relationship is when an object contains another object, a one-to-one (or many-to-one) relationship -- a "Monitor" object has a single "Manufacturer" object, a "Monitor" object has a single "CathodeRayTube" object. This relationship may be a 'dependent' relationship or not, SPOPS doesn't make a distinction. (A dependent relationship is one where the related object doesn't exist outside the context of the original one -- you probably wouldn't deal with a CRT without a monitor, but you would definitely deal with a manufacturer outside of a monitor.) The 'links_to' relationship is when an object is related to one or many other objects -- A "Manufacturer" object is related to multiple "Monitor" objects. Using a DBI datastore this is typically implemented with a linking table, but if you're dealing with dependent objects a linking table may be unnecessary. Two objects can mix the two relationships: while a "Monitor" may have a single "Manufacturer", a "Manufacturer" will have many "Monitors". Code GenerationRelationship methods are created when the SPOPS class is initialized. (See SPOPS::Manual::CodeGeneration for more information on this process.) The names of the methods generated depend on the type of the relationship and how it's configured, but they frequently depend on what's called the object alias. This is simply the key given in the configuration passed to SPOPS::Initialize or SPOPS::ClassFactory. For instance, in the following configuration we define three classes with the aliases 'user', 'book' and 'publisher':1: my %config = ( 2: book => { 3: class => 'My::Book', ... 4: }, 5: publisher => { 6: class => 'My::Publisher', ... 7: }, 8: user => { 9: class => 'My::User', ... 10: }, 11: ); 12: SPOPS::Initialize->process({ config => \%config }); You can always get the alias for a class by querying its configuration: 1: SPOPS::Initialize->process({ config => \%config }); 2: my $book_alias = My::Book->CONFIG->{main_alias}; 3: my $pub_alias = My::Publisher->CONFIG->{main_alias}; 4: my $user_alias = My::User->CONFIG->{main_alias}; MULTIPLE ID FIELDSNone of the automatically generated methods works with multi-field primary keys. To create a relationship you will need to write the method by hand.SPOPS GENERIC - USING 'has_a'ConfigurationHere are the potential 'has_a' configuration options:1: # Given: 2: 'contained' => { 3: class => 'My::ContainedClass', 4: id => 'contained_id', 5: } 6: 7: # Basic usage 8: has_a => { class-name => 'id-field' }, 9: has_a => { My::ContainedClass => 'contained_id' } 10: -- Creates method 'contained' 11: 12: # Other ID field name 13: has_a => { class-name => 'id-field' }, 14: has_a => { My::ContainedClass => 'original' } 15: -- Creates method 'original_contained' 16: 17: # Multiple ID fields 18: has_a => { class-name => [ 'id-field', 'id-field' ] }, 19: has_a => { My::ContainedClass => [ 'contained_id, 'original' ] } 20: -- Creates methods 'contained' and 'original_contained' 21: 22: # Specific method to create and a default 23: has_a => { class-name => { method-name => 'id-field' }, 'id-field' }, 24: has_a => { My::ContainedClass => 25: { 'originally_contained_by' => 'original' }, 26: 'contained_id' }, 27: -- Creates methods 'originally_contained_by' and 'contained' 28: 29: # Specific method to create and multiple other ID fields 30: has_a => { class-name => { method-name => 'id_field'}, 31: [ 'id-field', 'id-field' ] }, 32: has_a => { My::ContainedClass => 33: { 'originally_contained_by' => 'original' }, 34: [ 'contained_id', 'future' ] } 35: -- Creates methods 'originally_contained_by', 'contained' and 36: 'future_contained' The BasicsAll SPOPS objects can define a 'has_a' relationship. This is a one-to-one relationship between two objects. To use a canonical example, a book has a single publisher. (The reverse relationship, a publisher links to many books, will be discussed below.)Generally this is defined through an object containing the ID for another object as one of its values. Therefore, to specify the relationship you need:
To use the book and publisher example: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: has_a => { 'My::Publisher' => 'publisher_id' }, 11: } 1: 'publisher' => { 2: class => 'My::Publisher', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'publisher_id', 5: field_discover => 'yes', 6: base_table => 'publisher', 7: increment_field => 1, 8: no_insert => [ 'publisher_id' ], 9: no_update => [ 'publisher_id' ], 10: } So here we map the class we want our book object to contain ("My::Publisher") to the field in the book object which contains the ID of the object. Once we process this, we can call: 1: my $book = My::Book->fetch( $book_id ); 2: my $publisher = $book->publisher(); And retrieve the "My::Publisher" object contained in the $book object. This method "publisher()" is created at class initialization. (See SPOPS::Manual::CodeGeneration for more information on this process.) SPOPS knows to call the method "publisher" from the alias attached to the class "My::Publisher" and because the name of the ID field in the "My::Book" object is the same as the ID field in the "My::Publisher" object. More Complex Example: Different ID FieldMany times you will have a field that contains the ID of a contained object, but it's not the same name as the ID field of the contained object. For example, in your "My::Book" object you may have a field to contain the ID of the user who last updated the record. This field might be named 'updated_by' while the ID field for the "My::User" object is 'user_id'.To automatically create the relationship, you would add to your configuration so it looks like this: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: has_a => { 'My::Publisher' => 'publisher_id', 11: 'My::User' => 'updated_by' }, 12: } SPOPS would create a method 'updated_by_user' that would return the "My::User" object with the ID equal to the 'updated_by' field of the "My::Book" object. How did it create this method name? Without further customization (more below), SPOPS will take the field name originating the relationship ('updated_by'), append a '_' and then append the alias of the object being related to ('user'). updated_by + _ + user => updated_by_user This can be useful but somewhat clunky if you have long fieldnames and/or object aliases. So you can customize this by specifying the name of the method you'd like to create Say we wanted to call up the user who updated the "My::Book" object with the method 'updater'. To do this we'd change the configuration: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: has_a => { 'My::Publisher' => 'publisher_id', 11: 'My::User' => { updater => 'updated_by' } }, 12: } More Complex Example: More Than One Contained ObjectMany times you may have more than one of a particular type of object contained in another object. For example, say our publishing company bought the rights to a number of books that we want to republish under our own name. We want to keep the original publisher and the current publisher in separate fields. (We could also do this by creating a table to link the book and publisher tables, but that can get complicated quickly, and in this case it's unnecessary.)So after changing our schema we now have two publisher fields in our "My::Book" object: 'original_publisher_id' and 'current_publisher_id'. Here's what a first pass at the configuration would look like: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: has_a => { 'My::Publisher' => [ 'original_publisher_id', 11: 'current_publisher_id' ], 12: 'My::User' => { updater => 'updated_by' } }, 13: } This works, but the automatically created methods will be "original_publisher_id_publisher()" and "current_publisher_id_publisher()". Nasty. Let's fix that so we use the methods "original_publisher()" and "current_publisher()". 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: has_a => { 'My::Publisher' => [ { original_publisher => 'original_publisher_id' }, 11: { current_publisher => 'current_publisher_id' } ], 12: 'My::User' => { updater => 'updated_by' } }, 13: } It looks a little hairy, but you can see how we've built it up step by step. Fortunately, once you get the mapping down you never need to edit it again until a schema change, which is hopefully quite rare. SPOPS::DBI - USING 'links_to'ConfigurationHere are the potential 'links_to' configuration options:1: # Given: 2: 'contained' => { 3: class => 'My::ContainedClass', 4: id => 'contained_id', 5: } 6: 7: # Basic usage 8: links_to => { class-name => 'linking-table-name' } 9: links_to => { My::ContainedClass => 'contained_link' } 10: -- Creates method 'contained', 'contained_add' and 'contained_remove' The BasicsA 'links_to' relationship is one-to-many. (It can also be many-to-many if we look at it in both directions.) To continue with our example above, a single publisher links to many books.Generally this is defined by a linking table. For instance, assume you have the following scaled down schema: 1: CREATE TABLE book ( 2: book_id int not null, 3: name varchar(255) not null, 4: primary key( book_id ) 5: ) 6: 7: CREATE TABLE publisher ( 8: publisher_id int not null, 9: name varchar(255) not null, 10: primary key( publisher_id ) 11: ) 12: 13: CREATE TABLE publisher_book ( 14: publisher_id int not null, 15: book_id int not null, 16: primary key( publisher_id, book_id ) 17: ) The 'publisher_book' table acts to link the 'publisher' and 'book' tables. (In the real world, you'd probably make the relationship its own object since it would contain additional information about the relationship.) Using SQL, you'd fetch the books for a particular publisher with a statement like this: 1: SELECT book.book_id, book.name 2: FROM publisher pub, book book, publisher_book link 3: WHERE pub.publisher_id = ? 4: AND link.publisher_id = pub.publisher_id 5: AND book.book_id = link.book_id Since we're dealing with objects, we want to be able to perform something like this: 1: my $publisher = My::Publisher->fetch( $pub_id ); 2: my $books = $publisher->book; 3: print "Books published by $publisher->{name}:\n"; 4: foreach my $book ( @{ $books } ) { 5: print " $book->{name}\n"; 6: } The configuration to make this happen would look like this: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'book_id', 5: field_discover => 'yes', 6: base_table => 'book', 7: increment_field => 1, 8: no_insert => [ 'book_id' ], 9: no_update => [ 'book_id' ], 10: links_to => { 'My::Publisher' => 'publisher_book' }, 11: } 1: 'publisher' => { 2: class => 'My::Publisher', 3: isa => [ 'SPOPS::DBI::Pg', 'SPOPS::DBI' ], 4: id => 'publisher_id', 5: field_discover => 'yes', 6: base_table => 'publisher', 7: increment_field => 1, 8: no_insert => [ 'publisher_id' ], 9: no_update => [ 'publisher_id' ], 10: links_to => { 'My::Book' => 'publisher_book' }, 11: } Adding and Removing LinksWhen you define a 'links_to' relationship, SPOPS generates three methods:
The first one is covered above. The "_add()" and "_remove()" methods remove the link between two objects rather than the object itself. To use your example, removing a link between the book and publisher would delete the record out of the 'publisher_book' table but leave the associated 'publisher' and 'book' records unchanged. Code adding and removing a book from the publisher might look like: 1: my $publisher = My::Publisher->fetch( $pub_id ); 2: my $books = $publisher->book; 3: foreach my $book ( @{ $books } ) { 4: if ( $book->publication_date < 1990 ) { 5: $publisher->book_remove( $book ); 6: } 7: } 8: 9: my @book_ids = (); 10: open( REPORT, '< new_publications_report' ); 11: while ( <REPORT> ) { 12: chomp; 13: s/\s//g; 14: next if ( $_ eq '' ); 15: push @book_ids, $_; 16: } 17: $publisher->book_add( \@book_ids ); Advanced 'links_to' configurationYou can also specify many of the variables used in the code generation process yourself. For instance, your linking table may not use the same ID fields as either of your classes, or you may want to modify the names of the methods created.To do this pass a hashref instead of a table name in the 'links_to' configuration. For instance, if in our 'publisher_book' table the publisher ID was 'p_id' and the book ID was 'b_id' we would use: 1: links_to => { 'My::Book' => 2: { table => 'publisher_book', 3: to_id_field => 'b_id', 4: from_id_field => 'p_id', }, 5: } The fields we can define are:
SPOPS::LDAP - USING 'has_a'The basic idea is the same as the default implementation for 'has_a' -- -- the ID for the object is contained within the object being queried. (That is, I contain these DN's to which I'm related.) However, since SPOPS::LDAP objects can have multivalued fields it can store multiple IDs (in this case, distinguished names) and therefore relate to multiple objects. Therefore, we also define "_add()" and "_remove()" methods for each relationship.The relationship declaration is very similar: 1: 'book' => { 2: class => 'My::Book', 3: isa => [ 'SPOPS::LDAP' ], 4: multivalue => [ 'publisherLink' ], 5: has_a => { 'My::Publisher' => 'publisherLink' }, 6: } Here, we specify that we're holding DN records for "My::Publisher" objects in the field "publisherLink". We'd fetch, add and remove related LDAP objects similar to the DBI actions. Also similar to the DBI actions, we're not actually deleting the related object, just the link to the related object: 1: my $book = My::Book->fetch( "OpenInteract: The Manual" ); 2: foreach my $publisher ( @{ $book->publisher } ) { 3: if ( $publisher->{name} eq 'Wrox Press' ) { 4: $book->publisher_remove( $publisher ); 5: next; 6: } 7: $found_ora++ if ( $publisher->{name} eq "O'Reilly and Associates" ); 8: } 9: unless ( $found_ora ) { 10: $ora = My::Publisher->fetch( "O'Reilly and Associates" ); 11: $book->publisher_add( $ora ); 12: } 13: SPOPS::LDAP - USING 'links_to'This is the reverse of the 'has_a' idea -- the ID for this object is contained within a field of other objects. (That is, my DN is in other objects to which I'm related.) But similar to 'has_a' the methods "_add()" and "_remove()" are created in the code generation process. However, instead of modifying this object the "_add()" and "_remove()" methods remove the DN for this object from the other object's field.Here's a configuration snippet: 1: 'publisher' => { 2: class => 'My::Publisher', 3: isa => [ 'SPOPS::LDAP' ], 4: links_to => { 'My::Book' => 'publisherLink' }, 5: } And a brief usage example: 1: my $publisher = My::Publisher->fetch( "O'Reilly and Associates" ); 2: foreach my $book ( @{ $publisher->book } ) { 3: if ( $book->{subject} eq 'Perl' ) { 4: $book->{sales} *= 10; 5: } 6: if ( $book->{subject} eq '.NET' ) { 7: $publisher->book_remove( $book ); 8: } 9: } FUTURE DIRECTIONSRay Zimmerman has written up a much improved method for defining relationships between objects. This will be implemented before SPOPS 1.0, but time constraints make it impossible to specify when this will happen:http://www.geocrawler.com/archives/3/8393/2002/1/0/7464826/ COPYRIGHTCopyright (c) 2001-2004 Chris Winters. All rights reserved.See SPOPS::Manual for license. AUTHORSChris Winters <chris@cwinters.com>
Visit the GSP FreeBSD Man Page Interface. |