NAME
SPOPS::Manual::Relationships - SPOPS object relationships
SYNOPSIS
This document aims to answer the following questions:
-
How do I relate objects?
DESCRIPTION
Objects 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 Generation
Relationship 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 FIELDS
None 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'
Configuration
Here 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 Basics
All 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 Field
Many 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 Object
Many 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'
Configuration
Here 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 Basics
A '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 Links
When you define a 'links_to' relationship, SPOPS generates three methods:
-
$alias
- Returns an arrayref of related objects -
${alias}_add( $id | $object | \@id_list | \@object_list )
- Adds links to the related objects in$object
or\@object_list
or defined by the IDs in$id
or\@id_list
. -
{$alias}_remove( $id | $object | \@id_list | \@object_list )
- Removes links to the related objects in$object
or\@object_list
or defined by the IDs in$id
or\@id_list
.
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' configuration
You 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:
- table (required)
You must still specify the table you're using as a linking table.
- to_id_field (optional)
If the ID field of the object you're linking to is different than configured in the class you can specify it here. So in the above example the object linking to is 'book' and the new ID field for the book was 'b_id'.
- from_id_field (optional)
If the ID field of the object you're linking from is different than configured in the class you can specify it here. So in the above example the object linking from is 'publisher' and the new ID field for the book was 'p_id'.
- alias (optional)
You can change the names of the generated methods using this value. Instead of using the main alias of the class as configured you can specify something new here. So if you wanted the methods to be generated for your Spanish-speaking developers you can set this to 'libro' and the methods generated would be 'libro', 'libro_add' and 'libro_remove' instead of 'book', 'book_add' and 'book_remove'.
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 DIRECTIONS
Ray 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/
COPYRIGHT
Copyright (c) 2001-2004 Chris Winters. All rights reserved.
See SPOPS::Manual for license.
AUTHORS
Chris Winters <chris@cwinters.com>
Generated from the SPOPS 0.87 source.