Associations
Real applications rarely deal with a single table in isolation — an article has an author, and a comment section. CakePHP calls the relationships between tables associations, and once you define them, the ORM can fetch related data for you automatically instead of you writing manual joins or extra queries by hand.
Where Associations Are Defined
Associations are declared inside a Table class's initialize() method — the same
ArticlesTable class from Models & the ORM where
custom finder methods live.
The Main Association Types
| Association | Meaning | Example |
|---|---|---|
belongsTo | This table's rows each belong to one row in another table | An Article belongs to one Author |
hasMany | This table's rows can each have many related rows elsewhere | An Article has many Comments |
belongsToMany | A many-to-many relationship, usually through a join table | Articles and Tags, linked through an articles_tags table |
Worked Example: Defining Associations
Here's ArticlesTable::initialize() declaring that an article belongs to one author and
has many comments:
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->belongsTo('Authors');
$this->hasMany('Comments');
}
}
Each call names the related Table class — CakePHP infers the foreign key
(author_id on articles for the belongsTo, and
article_id on comments for the hasMany) from convention, the
same convention-over-configuration approach used throughout CakePHP.
Fetching Related Data With contain()
Defining an association doesn't load related data by itself — a plain
$this->Articles->find()->all() still only returns article rows. To eager-load
the related authors and comments in the same query pass, add contain():
$articles = $this->Articles->find()
->contain(['Authors', 'Comments'])
->all();
Now every $article in the result also carries $article->author and
$article->comments, already populated.
Why contain() Matters for Performance
Without contain(), getting each article's author would mean running a separate query
per article inside a loop — the classic N+1 query problem: 1 query for the
articles, plus N more queries (one per article) for their authors. With 50 articles, that's 51
queries hitting the database instead of 2.
contain() issues one additional, efficient query per
associated table (not per row), regardless of how many articles are returned — so fetching authors
and comments for 50 articles still takes just 3 queries total, not 101.