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

AssociationMeaningExample
belongsToThis table's rows each belong to one row in another tableAn Article belongs to one Author
hasManyThis table's rows can each have many related rows elsewhereAn Article has many Comments
belongsToManyA many-to-many relationship, usually through a join tableArticles 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.

Note: 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.
Next steps: Data you accept from forms — including nested data for associated records — needs rules to make sure it's actually valid before it's saved. Head to Validation to see how CakePHP handles that.