Understanding CakePHP Associations

Posted on Jun 06 2007 in Web Development

CakePHP uses the Model-View-Controller (MVC) concept as a framework to encapsulate the three basic layers of an application. Models, the component of the MVC framework that connect the application to the data, are especially good at describing relational databases. One of the questions that seems to come up alot on the CakePHP Group in one form or another, is what the practical difference is between the various relationship types (or associations) that CakePHP models can implement. I'll try to explain what's helped me keep them straight...

NOTE: This is not a tutorial on writing a CakePHP application, but rather a few tips for understanding associations. For more complete information on writing a CakePHP application, see the manual.

I remember studying basic genetics in biology in high school, and our teacher said that genetics is one of those topics that people have an extremely hard time understanding... until it just clicks -- then you can't understand why you didn't get it before. I feel that learning associations in CakePHP can be a similar issue. I've been writing relational database applications for several years, but I remember when I was defining models in my first Cake app... I couldn't for the life of me get a grip on what the difference between "hasOne" and "belongsTo" was, let alone the infamous hasAndBelongsToMany! That was, until I took a closer look at the CakePHP Manual chapter on Models and made an important discovery: the difference between hasOne and belongsTo lies in which table in the relationship does the 'pointing.'

belongsTo:

A -> B

If table A has a field that references table B, then table A is said to "belongTo" table B.

Just imagine that every record in the A table says "I belong to a record in table B!"

In the movie database example below, since for each movie there is one director, and the movie table has a field that points to a director, each movie belongs to a director. With belongsTo, each record can only be associated with one foreign record, but multiple records can be associated with any given foreign record. For instance, all three Matrix movies were directed by the Wachowski brothers, so each movie would belong to the Brothers' entry in the directors table.

hasOne:

A <- B

If only a single record in table B has a field that references table A, then table A is said to "hasOne" table B.

Every record in table A says: "I have one record in table B that points to me!"

hasOne is basically belongsTo, but the pointing goes in the other direction. If table A hasOne table B, then it can also be said that table B belongsTo table A (except only one B record would be associated with any A record).

In all honesty, I don't think I've ever used this association. This is because if every record in table A only has one record associated in table B, the tables may be able to be combined. Where the use for this association comes in is when you have 1-to-1 relations, but the data may be best kept in different tables for logical or technical reasons. For instance, in the CakePHP manual blog example, every user hasOne profile -- in theory, the tables could be combined, but it probably is easier to keep them apart and accessible through association.

hasMany:

A <- B (multiple)

If table B has a field that references table A, and multiple records in table B can point to the same A record, then table A is said to "hasMany" table B.

Every record in table A says: "I have many records in table B that point to me!"

hasMany is the true sibling to belongsTo. If table A has many B, then table B can be said to belongTo A (and the multiple works, since A has MANY). hasMany and belongsTo are very complimentary if you need to get to a relationship from each side of it.

hasAndBelongsToMany:

A <-> C <-> B (multiple)

If every record in table A can link to multiple references in table B, and every record in table B can be linked to by multiple A records, table A is said to "hasAndBelongToMany" table B.

HABTM is a little more complicated, because it uses a link table. A link table will have at least two fields, one to point to the id of the first table, and another to point to the id of the linked record in the second table. In our movie database example below, since a single movie can have multiple genres, and you can have more than one movie in any given genre, the movie table has many and belongs to the genre table. HABTM associations can go both ways since the link table will work in each direction, so they can be (and often are) defined in both models (as it is done below).

Let's consider a sample application where we're developing a small movie database (an extremely small step-brother to IMDb). This application will be a database of movies categorized by genre with directors and movie reviews. Each movie can be in multiple genres (Action/Horror, Sci-Fi/Thriller, etc), can have one director (we'll ignore multiple directors for now), and have multiple reviews.

We'll start by outlining our models:

Movie:
  1. belongsTo Director
  2. hasMany Reviews
  3. hasAndBelongsToMany Genres
Director:
  1. hasMany Movies
Review:
  1. belongsTo Movie
Genre:
  1. hasAndBelongsToMany Movies

And now some code:

class Movie extends AppModel {
    var $name = 'Movie';
    var $belongsTo = array( 'Director' => array( 'className' => 'Director' ) );
    var $hasMany = array( 'Reviews' => array( 'className' => 'Review' ) );

    // We don't need many settings in this association, because we'll stick to Cake's naming conventions
    var $hasAndBelongsToMany = array( 'Genres' => array( 'className' => 'Genre' ) );
}

class Director extends AppModel {
    var $name = 'Director' ;
    $hasMany = array( 'Movie' => array( 'className' => 'Movie' ) );
}

class Review extends AppModel {
    var $name = 'Review';
    $belongsTo = array( 'Movie' => array( 'className' => 'Movie' ) );
}

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

    // We specify the join table here because Cake would expect the table to be called genres_movies from this side
    $hasAndBelongsToMany = array( 'Movies' => array( 'className' => 'Movie',
                                                     'joinTable' => 'movies_genres'
                                                   );
}

And for good measure, here's a basic outline of the table structure:

movies:
  • id
  • title
  • release_date
  • director_id
directors:
  • id
  • first_name
  • last_name
reviews:
  • id
  • movie_id
  • author_name
  • body_text
genres:
  • id
  • name
genres_movies:
  • id
  • movie_id
  • genre_id

Using these models and tables, we can use this line of code in a controller:

$movie = $this->Movie->read(null, $id); //assuming $id contains a movie id...

Will gives us a data structure like this in the $movie variable:

Array (
    [id] =>
    [title] =>
    [release_date] =>
    [director_id] =>
    [Director] => Array (
        [id] =>
        [first_name] =>
        [last_name] =>
    )
    [Reviews] => Array (
        [0] => Array (
            [id] =>
            [movie_id] =>
            [author_name] =>
        )
        [1] => Array (
            [id] =>
            [movie_id] =>
            [author_name] =>
        )
    )
    [Genres] => Array (
        [0] => Array (
            [id] =>
            [name] =>
        )
        [1] => Array (
            [id] =>
            [name] =>
        )
    )
)