Advanced Relationships
Datamapper ORM has extended the ability of DataMapper to handle significantly more complex relationships, including:
- Multiple Relationships to the Same Model
- Proper Self-Relationships
- Storing Additional Information on the Join Table
- Overriding the standard naming conventions
- Defining relationships after the model has been loaded
More Advanced Relationship Overview
Before showing examples, let's review a normal relationship, and the information needed to make it work. A normal relationship between two models is managed by the database structure, and a value stored on both models, in the $has_many and $has_one arrays. This value tells DataMapper to look for the related model. Internally, DataMapper knows that this model is related using the generated table and model names.
With advanced relationships, we can override the generated information, and even replace the name used to look for the relationship. This allows us to relate the same object multiple times, as well as relate an object to itself.
Extended Relationship Attributes
Previously, a single value was stored per object in the $has_many and $has_one arrays. To begin making more advanced relationships, we convert them into a relationship key and attribute array():
Before
class Post extends DataMapper { $has_one = array('user'); }
After
class Post extends DataMapper { $has_one = array('user'=> array() ); }
Right now, nothing different will happen. User will still be related to Post as $post->user. To change the user into something else, we'll need to use some of the following four attributes. You can specify any combination of them, but the most common are class and other_field together.
Relationship name
The key of the relationship array entry defines the relationship name. It is the one used in the code when you want to query the relation. In the example given above, the key used is user, so you would access this relation by using:
// define a new relationship for the Posts model $p = new Post(1); $p->user->get();
Attributes Table
Name | Description | Default | Common Usage |
---|---|---|---|
Tells DataMapper which class (model) this relationship points to. | key used on the array entry | Almost always specified. Use it when the class name differs from the key used. | |
other_field | Tells DataMapper what the relationship looks like from class. | $this->model | Whenever the key relation of the other model is different than this models name. |
join_other_as | Override the generated column name for the other model. | key on $has_many or $has_one | Used with custom column names, or in some unusual self-relationships. |
join_self_as | Override the generated column name for this model. | other_field | Used with custom column names, or in some unusual self-relationships. |
join_table | Override the generated table name for this relation. | '' | Custom table name. Also used in some multi-table (more than two) relationships. |
model_path | Alternative location for this model. | APPPATH | Used in a modular context, where models are used cross-module. Note that is it the module path, NOT the model path! |
auto_populate | Autopopulate the objects for this relation. | FALSE | Use the same as the global settings $auto_populate_has_one and $auto_populate_has_many, but then on a per-relationship basis. |
cascade_delete | Delete the relation when deleting the parent object. | TRUE | Use the same as the configuration variable and the global setting $cascade_delete, but then on a per-relationship basis. |
Manually defining the relationship
Under normal circumstances, this is not something you should do, but it will help you understand how Datamapper generates the relationship definition between two models, and what the role of the different fields in the relationship definition is.
Consider the models Author and Book. An author can write multiple books, a book can have multiple writers, which makes this a many-to-many relationship. You can define this by a simple has_many entry in both models, and Datamapper will generate the rest. The following definition shows you the advanced relationship definition that Datamapper will generate internally for this relation.
Author
class Author extends DataMapper { $has_many = array( 'book' => array( // in the code, we will refer to this relation by using the object name 'book' 'class' => 'book', // This relationship is with the model class 'book' 'other_field' => 'author', // in the Book model, this defines the array key used to identify this relationship 'join_self_as' => 'author', // foreign key in the (relationship)table identifying this models table. The column name would be 'author_id' 'join_other_as' => 'book', // foreign key in the (relationship)table identifying the other models table as defined by 'class'. The column name would be 'book_id' 'join_table' => 'authors_books') // name of the join table that will link both Author and Book together ); }
Book
class Book extends DataMapper { $has_many = array( 'author' => array( // in the code, we will refer to this relation by using the object name 'author' 'class' => 'author', // This relationship is with the model class 'author' 'other_field' => 'book', // in the Author model, this defines the array key used to identify this relationship 'join_self_as' => 'book', // foreign key in the (relationship)table identifying this models table. The column name would be 'book_id' 'join_other_as' => 'author', // foreign key in the (relationship)table identifying the other models table as defined by 'class'. The column name would be 'author_id' 'join_table' => 'authors_books') // name of the join table that will link both Author and Book together ); }
Some common naming rule deviations
You want to add a prefix or suffix to your class names to avoid class name collisions (with for example controllers or CodeIgniter libraries).
- If you would use the '_model' prefix on your classes, use the class names 'book_model' and 'author_model' instead.
- You would still use $obj->book and $obj->author to access the relationship.
You want to use alternative names to specify the relationship in the code. Say want to use $obj->writer instead of $obj->author.
- In the Author class, use 'writer' for the other_field value.
- In the Book class, use 'writer' for the array key value.
You want to use alternative table name for the relation. For example, you prefer 'written_books'.
- In both the Author and the Book classes, use 'written_books' for the join_table value.
You want to use alternative name for a foreign key column name. Say want to use writer_id instead of author_id.
- In the Author class, use 'writer' for the join_self_as value.
- In the Book class, use 'writer' for the join_other_as value.
Multiple Relationships to the Same Model
This is the most common usage, and is used in almost every project. There is a simple pattern to defining this relationship.
Post has a creator and an editor, which may be different users. Here's how to set that up.
Post
class Post extends DataMapper { $has_one = array( 'creator' => array( 'class' =>'user', 'other_field' => 'created_post' ), 'editor' => array( 'class' =>'user', 'other_field' => 'edited_post' ) ); }
User
class User extends DataMapper { $has_many = array( 'created_post' => array( 'class' =>'post', 'other_field' => 'creator' ), 'edited_post' => array( 'class' =>'post', 'other_field' => 'editor' ) ); }
A couple of things to note here.
- The relationship is now defined by the relationship key on either side, not the model name. This has now become the only way to look up the relationship.
- The key on one side of the relationship becomes the other_field on the opposite side, and vice-versa.
- Because we need a way to specify the difference between posts that were edited and those that were created, we have to declare the slightly unusual edited_post and created_post relationships. These could have any name, as long as they were unique and mirrored on Post.
- The table structure is going to be a little different now.
Many-to-Many Reciprocal Self Relationships
In this type of relationship, you have records that related to each other, and where saving or deleting the relationship in one direction should also maintain the relationship in the reverse direction.
The best example I can think of is in the area of genealogy.
Suppose you have a table with information about people, and you want to relate them (grandparent-parent-child etc). You always have to make the relation both ways, since it can’t be that person A is the child of person B, but B is not the parent of A. If you define this relation as reciprocal, when you save the relation between A and B, automatically the relation between B and A is saved as well. And when you delete the relationship, both relations are deleted.
class Person extends DataMapper { $has_many = array( 'related_person' => array( 'class' =>'person', 'other_field' => 'person', 'reciprocal' => TRUE ), 'person' => array( 'other_field' => 'related_person', 'reciprocal' => TRUE ) ); }
To get this to work, you will need:
- You need a relationship table to define this many-to-many relation, which is this case would be called 'persons_persons'.
- This table needs to contain a column called 'person_id' and a column called 'related_person_id'.
Note that this defined per relation. If you want this to work both ways, you need to specify the 'reciprocal' setting on BOTH relationship definitions.
Setting up the Table Structure with Advanced Relationships
The table structure has one key difference. While the names of the tables is still determined using the plural form of the model, the column names are now defined using the relationship key.
In-table Foreign Keys
If we decide to use in-table foreign keys, the posts table looks like this:
id | title | body | creator_id | editor_id |
---|---|---|---|---|
1 | Hello World | My first post | 4 | 4 |
2 | Another Post | My second post (Edited by Joe) | 4 | 6 |
Dedicated Join Table
If we decide to use a join table, that table is a little different. The table is still called posts_users, but the table now looks like this:
id | creator_id | created_post_id | editor_id | edited_post_id |
---|---|---|---|---|
1 | 4 | 1 | NULL | NULL |
2 | NULL | NULL | 4 | 1 |
3 | 4 | 2 | NULL | NULL |
4 | NULL | NULL | 6 | 2 |
This stores the same information. We only have the option in this case because the posts side was $has_one. If posts could have many creators or many editors, then that would have to be stored in this table.
Multi-table Relationships
In normal circumstances, you will always have a relationship between two tables, either in a one-to-one, one-to-many, or many-to-many relationship.
Occasionally however, you might have a need to define a relationship between more than two tables. In this case, you will have to create a joined table with more than two foreign keys, one to each of the tables involved in the relationship. In this situation Datamapper can no longer automatically generate the name of the joined table. Instead, you will have to use the join_table to manually define the name of the joined table.
When you use this option, make sure you define the same table name in all models!
Self Relationships
Technically, self-relationships are the same as having multiple relationships to the same object. There is one key difference: the table names. First, we'll set the class up, then I'll show you the table name.
Post has Many Related Posts
We want to have the ability to track related posts. Here's the model:
class Post extends DataMapper { $has_one = array( 'creator' => array( 'class' => 'user', 'other_field' => 'created_post' ), 'editor' => array( 'class' => 'user', 'other_field' => 'edited_post' ) ); $has_many = array( 'relatedpost' => array( 'class' => 'post', 'other_field' => 'post' ), 'post' => array( 'other_field' => 'relatedpost' ) ); }
Some notes about this form:
- This shows how you can still have one side of the relationship retain the model name. In this case, $post->post will show the up-stream relationships, while $post->relatedpost shows the downstream.
- This is a Many to Many relationship, so we'll need a dedicated table.
- This is currently a one-way relationship, so each related post will have to be saved inversely as well.
Naming Self-Relationship Tables
Self relationships are special because the join table name is not generated from the table name of the object, but instead from the
For the example above, the table looks like this:
posts_relatedposts
id | post_id | relatedpost_id |
---|---|---|
1 | 1 | 2 |
2 | 2 | 1 |
This allows us to relate Post #1 -> Post #2, as well as relating Post #2 -> Post #1.
Defining relations after the model has been loaded
Sofar, all relationship definitions have been static, defined as a property of the datamapper model.
For most applications, this works fine. However, there are situation where you would like to define the relationships between models at runtime. This is particularly true in a modular environment, where a module can introduce new models that have a relationship with models from an other module, or from the application itself.
Obviously you can't define these relationships in the model itself, as the name of the model might not be known at the time of writing, or the models database structure might not be available until the module has been installed.
Runtime relationship definition
Until now, to define a relationship you are used to code it like this:
class Post extends DataMapper { $has_one = array('user'); }
This relation can also be defined at runtime:
// define a new relationship for the Posts model $p = new Post(); $p->has_one('user');
You can also do this for advanced relationships. The example used previously
class Post extends DataMapper { $has_one = array( 'creator' => array( 'class' =>'user', 'other_field' => 'created_post' ), 'editor' => array( 'class' =>'user', 'other_field' => 'edited_post' ) ); }
can be defined at runtime like this:
// define new relationships for the Posts model $p = new Post(); $relation = array( 'class' =>'user', 'other_field' => 'created_post' ); $p->has_one('creator', $relation); $relation = array( 'class' =>'user', 'other_field' => 'edited_post' ); $p->has_one('editor', $relation);
And there you have it. Advanced relationships to allow you to manage more complex data structures. On to DataMapper in Controllers so we can actually use this information!