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:

  • the type of object contained (class)

  • the ID field(s) defining the object contained

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_link' },
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: }

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 );

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-2002 Chris Winters. All rights reserved.

See SPOPS::Manual for license.

AUTHORS

Chris Winters <chris@cwinters.com>