Tuesday, February 9, 2010

CakePHP hasAndBelongsToMany (HABTM) Join

Coming back to one of the most critical basics of Cake  - it is hasAndBelongsToMany (HABTM) relationship. You may know it is a type of relationship between two different database tables (Models) in CakePHP. A case study will explain its importance:

Say, you have Category model and Post model.
Each category has many posts and each post belongs to more than one categories.

Had there been only one category per post, it would be enough to use category_id field in posts table. But we are talking about posts each of which may have more than one categories. In this case besides categories and posts table, you need one extra table. By convention - this table should be named as categories_posts (note the alphabetical order of join tables). The table categories_posts will have following fields : id, category_id, post_id.

In your post model define the relationship.


class Post extends AppModel {
  var $name = 'Post';
  var $hasAndBelongsToMany = array(
  'Category' =>
  'className'              => 'Category',
  'joinTable'              => 'categories_posts',
  'foreignKey'             => 'post_id',
  'associationForeignKey'  => 'category_id'


This will show you all categories for a given post when you fire:


Now the biggest question - how to SAVE records for HABTM association.

The default CakePHP functioning in this regard is not at all adequate. Say you want to add a new category to an existing post which already has a category defined by HABTM association - by default Cake will delete your existing category records for that post from categories_posts table before inserting the new record. That's an absolute mess if you do not really intend to desire existing records.  However, there is the savior.

ExtendAssociations Behavior.
This behavior allows you to easily add or delete HABTM associations!

A full detail of this Behavior can be found in Bakery!

You can download code for this behavior, and save that code as 'extend_associations.php' under '/app/models/behaviors' folder.

In your Post model, add the following code:

var $actsAs = 'ExtendAssociations'; 

So, our modified Post model should look like this:

class Post extends AppModel { 
var $name = 'Post'; 

var $actsAs = 'ExtendAssociations'; 
var $hasAndBelongsToMany = array( 
'Category' => array( 
'className' => 'Category', 
'joinTable' => 'categories_posts', 
'foreignKey' => 'post_id', 
'associationForeignKey' => 'category_id', 


Defining ACTION in PostsController
file: // app/controllers/posts_controller.php
Now in your PostsController - to ADD categories to a post use:

// to add only one category for a given post (say, post_id = 1)
$this->Post->habtmAdd('Category', 1, 1); 
// to add multiple categories 
$this->Post->habtmAdd('Category', 1, array(1, 2, 3)); 

// to DELETE categories for a given post (say, post_id = 1)

// delete a category 
$this->Post->habtmDelete('Category', 1, 1); 
// to delete multiple categories
$this->Post->habtmDelete('Category', 1, array(1, 3)); 
//  to delete all categories 
$this->Post->habtmDeleteAll('Tag', 1); 

That's it.
Good night.
Sweet Baking.


Mark said...

I'm new to cakephp... I presume you have to declare the HABTM variable in the Category class with 'foreignKey' => 'category_id',
'associationForeignKey' => 'post_id' as well? Also what does $this->Recipe->find(); have to do with it? I think this should be $this->Post->find(); Thanks, Mark.

Nilz said...

Thanks. It was written on the fly...