GSP
Quick Navigator

Search Site

Unix VPS
A - Starter
B - Basic
C - Preferred
D - Commercial
MPS - Dedicated
Previous VPSs
* Sign Up! *

Support
Contact Us
Online Help
Handbooks
Domain Status
Man Pages

FAQ
Virtual Servers
Pricing
Billing
Technical

Network
Facilities
Connectivity
Topology Map

Miscellaneous
Server Agreement
Year 2038
Credits
 

USA Flag

 

 

Man Pages
SPOPS::Manual::Relationships(3) User Contributed Perl Documentation SPOPS::Manual::Relationships(3)

SPOPS::Manual::Relationships - SPOPS object relationships

This document aims to answer the following questions:
How do I relate objects?

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".

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

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.

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'

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.

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: }

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.

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'

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: }
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 );
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'.

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:

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: }

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 (c) 2001-2004 Chris Winters. All rights reserved.

See SPOPS::Manual for license.

Chris Winters <chris@cwinters.com>
2004-06-02 perl v5.32.1

Search for    or go to Top of page |  Section 3 |  Main Index

Powered by GSP Visit the GSP FreeBSD Man Page Interface.
Output converted with ManDoc.