3.7.6.5 hasAndBelongsToMany (HABTM)
Alright. At this point, you can already call yourself a CakePHP model associations professional. You’re already well versed in the three associations that take up the bulk of object relations.
Let’s tackle the final relationship type: hasAndBelongsToMany, or HABTM. This association is used when you have two models that need to be joined up, repeatedly, many times, in many different ways.
The main difference between hasMany and HABTM is that a link between models in HABTM is not exclusive. For example, we’re about to join up our Recipe model with a Tag model using HABTM. Attaching the “Italian” tag to my grandma’s Gnocci recipe doesn’t “use up” the tag. I can also tag my Honey Glazed BBQ Spaghettio’s with “Italian” if I want to.
Links between hasMany associated objects are exclusive. If my User hasMany Comments, a comment is only linked to a specific user. It’s no longer up for grabs.
Moving on. We’ll need to set up an extra table in the database to handle HABTM associations. This new join table’s name needs to include the names of both models involved, in alphabetical order, and separated with an underscore ( _ ). The contents of the table should be at least two fields, each foreign keys (which should be integers) pointing to both of the primary keys of the involved models.
HABTM requires a separate join table that includes both model names.
| Relation | Schema |
|---|---|
| Recipe HABTM Tag | id, recipes_tags.recipe_id, recipes_tags.tag_id |
| Cake HABTM Fan | id, cakes_fans.cake_id, cakes_fans.fan_id |
| Foo HABTM Bar | id, bars_foos.foo_id, bars_foos.bar_id |
Table name are by convention in alphabetical order.
Once this new table has been created, we can define the HABTM association in the model files. We’re gonna skip straight to the array syntax this time:
<?php
class Recipe extends AppModel {
var $name = 'Recipe';
var $hasAndBelongsToMany = array(
'Tag' =>
array(
'className' => 'Tag',
'joinTable' => 'recipes_tags',
'with' => '',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'tag_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
?>
<?phpclass Recipe extends AppModel {var $name = 'Recipe';var $hasAndBelongsToMany = array('Tag' =>array('className' => 'Tag','joinTable' => 'recipes_tags','with' => '','foreignKey' => 'recipe_id','associationForeignKey' => 'tag_id','unique' => true,'conditions' => '','fields' => '','order' => '','limit' => '','offset' => '','finderQuery' => '','deleteQuery' => '','insertQuery' => ''));}?>
Possible keys for HABTM association arrays include:
- className: the classname of the model being associated to the current model. If you’re defining a ‘User hasMany Comment’ relationship, the className key should equal ‘Comment.’
- joinTable: The name of the join table used in this association (if the current table doesn’t adhere to the naming convention for HABTM join tables).
- with: Defines the name of the model for the join table. By default CakePHP will auto-create a model for you. Using the example above it would be called RecipesTag. By using this key you can override this default name. The join table model can be used just like any "regular" model to access the join table directly.
- foreignKey: the name of the foreign key found in the current model. This is especially handy if you need to define multiple HABTM relationships. The default value for this key is the underscored, singular name of the current model, suffixed with ‘_id’.
- associationForeignKey: the name of the foreign key found in the other model. This is especially handy if you need to define multiple HABTM relationships. The default value for this key is the underscored, singular name of the other model, suffixed with ‘_id’.
- unique: If true (default value) cake will first delete existing relationship records in the foreign keys table before inserting new ones, when updating a record. So existing associations need to be passed again when updating.
- conditions: An SQL fragment used filter related model records. It’s good practice to use model names in SQL fragments: “Comment.status = 1” is always better than just “status = 1.”
- fields: A list of fields to be retrieved when the associated model data is fetched. Returns all fields by default.
- order: An SQL fragment that defines the sorting order for the returned associated rows.
- limit: The maximum number of associated rows you want returned.
- offset: The number of associated rows to skip over (given the current conditions and order) before fetching and associating.
- finderQuery, deleteQuery, insertQuery: A complete SQL query CakePHP can use to fetch, delete, or create new associated model records. This should be used in situations that require very custom results.
Once this association has been defined, find operations on the Recipe model will also fetch related Tag records if they exist:
//Sample results from a $this->Recipe->find() call.
Array
(
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Breakfast
)
[1] => Array
(
[id] => 124
[name] => Dessert
)
[2] => Array
(
[id] => 125
[name] => Heart Disease
)
)
)
Remember to define a HABTM association in the Tag model if you'd like to fetch Recipe data when using the Tag model.
It is also possible to execute custom find queries based on HABTM relationships. Consider the following examples:
Assuming the same structure in the above example (Recipe HABTM Tag), let's say we want to fetch all Recipes with the tag 'Dessert', one potential (wrong) way to achieve this would be to apply a condition to the association itself:
$this->Recipe->bindModel(array(
'hasAndBelongsToMany' => array(
'Tag' => array('conditions'=>array('Tag.name'=>'Dessert'))
)));
$this->Recipe->find('all');
$this->Recipe->bindModel(array('hasAndBelongsToMany' => array('Tag' => array('conditions'=>array('Tag.name'=>'Dessert')))));$this->Recipe->find('all');
//Data Returned
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 124
[name] => Dessert
)
)
)
1 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Crab Cakes
[created] => 2008-05-01 10:31:01
[user_id] => 2349
)
[Tag] => Array
(
}
}
}
Notice that this example returns ALL recipes but only the "Dessert" tags. To properly achieve our goal, there are a number of ways to do it. One option is to search the Tag model (instead of Recipe), which will also give us all of the associated Recipes.
$this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
$this->Recipe->Tag->find('all', array('conditions'=>array('Tag.name'=>'Dessert')));
We could also use the join table model (which CakePHP provides for us), to search for a given ID.
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions'=>array('RecipesTag.tag_id'=>124) // id of Dessert
));
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));$this->Recipe->find('all', array('fields' => array('Recipe.*'),'conditions'=>array('RecipesTag.tag_id'=>124) // id of Dessert));
It's also possible to create an exotic association for the purpose of creating as many joins as necessary to allow filtering, for example:
$this->Recipe->bindModel(array(
'hasOne' => array(
'RecipesTag',
'FilterTag' => array(
'className' => 'Tag',
'foreignKey' => false,
'conditions' => array('FilterTag.id = RecipesTag.id')
))));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions'=>array('FilterTag.name'=>'Dessert')
));
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag','FilterTag' => array('className' => 'Tag','foreignKey' => false,'conditions' => array('FilterTag.id = RecipesTag.id')))));$this->Recipe->find('all', array('fields' => array('Recipe.*'),'conditions'=>array('FilterTag.name'=>'Dessert')));
Both of which will return the following data:
//Data Returned
Array
(
0 => Array
{
[Recipe] => Array
(
[id] => 2745
[name] => Chocolate Frosted Sugar Bombs
[created] => 2007-05-01 10:31:01
[user_id] => 2346
)
[Tag] => Array
(
[0] => Array
(
[id] => 123
[name] => Breakfast
)
[1] => Array
(
[id] => 124
[name] => Dessert
)
[2] => Array
(
[id] => 125
[name] => Heart Disease
)
)
}
For more information on binding model associations on the fly see Creating and destroying associations on the fly
Mix and match techniques to achieve your specific objective.
