Associations - Linking Tables Together

Defining relations between different objects in your application should bea natural process. For example, an article may have many comments, and belongto an author. Authors may have many articles and comments. CakePHP makesmanaging these associations easy. The four association types in CakePHP are:hasOne, hasMany, belongsTo, and belongsToMany.

RelationshipAssociation TypeExample
one to onehasOneA user has one profile.
one to manyhasManyA user can have multiple articles.
many to onebelongsToMany articles belong to a user.
many to manybelongsToManyTags belong to many articles.

Associations are defined during the initialize() method of your tableobject. Methods matching the association type allow you to define theassociations in your application. For example if we wanted to define a belongsToassociation in our ArticlesTable:

  1. namespace App\Model\Table;
  2.  
  3. use Cake\ORM\Table;
  4.  
  5. class ArticlesTable extends Table
  6. {
  7. public function initialize(array $config)
  8. {
  9. $this->belongsTo('Authors');
  10. }
  11. }

The simplest form of any association setup takes the table alias you want toassociate with. By default all of the details of an association will use theCakePHP conventions. If you want to customize how your associations are handledyou can modify them with setters:

  1. class ArticlesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->belongsTo('Authors', [
  6. 'className' => 'Publishing.Authors'
  7. ])
  8. ->setForeignKey('authorid')
  9. ->setProperty('person');
  10. }
  11. }

You can also use arrays to customize your associations:

  1. $this->belongsTo('Authors', [
  2. 'className' => 'Publishing.Authors',
  3. 'foreignKey' => 'authorid',
  4. 'propertyName' => 'person'
  5. ]);

Arrays, however, do not offer the typehinting and autocomplete benefit, the fluent interface does.

The same table can be used multiple times to define different types ofassociations. For example consider a case where you want to separateapproved comments and those that have not been moderated yet:

  1. class ArticlesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasMany('Comments')
  6. ->setConditions(['approved' => true]);
  7.  
  8. $this->hasMany('UnapprovedComments', [
  9. 'className' => 'Comments'
  10. ])
  11. ->setConditions(['approved' => false])
  12. ->setProperty('unapproved_comments');
  13. }
  14. }

As you can see, by specifying the className key, it is possible to use thesame table as different associations for the same table. You can even createself-associated tables to create parent-child relationships:

  1. class CategoriesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasMany('SubCategories', [
  6. 'className' => 'Categories'
  7. ]);
  8.  
  9. $this->belongsTo('ParentCategories', [
  10. 'className' => 'Categories'
  11. ]);
  12. }
  13. }

You can also setup associations in mass by making a single call toTable::addAssociations() which accepts an array containing a set oftable names indexed by association type as an argument:

  1. class PostsTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->addAssociations([
  6. 'belongsTo' => [
  7. 'Users' => ['className' => 'App\Model\Table\UsersTable']
  8. ],
  9. 'hasMany' => ['Comments'],
  10. 'belongsToMany' => ['Tags']
  11. ]);
  12. }
  13. }

Each association type accepts multiple associations where the keys are thealiases, and the values are association config data. If numeric keys are usedthe values will be treated as association aliases.

HasOne Associations

Let’s set up a Users Table with a hasOne relationship to an Addresses Table.

First, your database tables need to be keyed correctly. For a hasOnerelationship to work, one table has to contain a foreign key that points to arecord in the other. In this case the addresses table will contain a fieldcalled user_id. The basic pattern is:

hasOne: the other model contains the foreign key.

RelationSchema
Users hasOne Addressesaddresses.user_id
Doctors hasOne Mentorsmentors.doctor_id

Note

It is not mandatory to follow CakePHP conventions, you can override the useof any foreignKey in your associations definitions. Nevertheless stickingto conventions will make your code less repetitive, easier to read and tomaintain.

If we had the UsersTable and AddressesTable classes made we could makethe association with the following code:

  1. class UsersTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasOne('Addresses');
  6. }
  7. }

If you need more control, you can define your associations using the setters.For example, you might want to limit the association to include only certainrecords:

  1. class UsersTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasOne('Addresses')
  6. ->setName('Addresses')
  7. ->setConditions(['Addresses.primary' => '1'])
  8. ->setDependent(true);
  9. }
  10. }

If you want to break different addresses into multiple associations, you can do something like:

  1. class UsersTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasOne('HomeAddress', [
  6. 'className' => 'Addresses'
  7. ])
  8. ->setProperty('home_address')
  9. ->setConditions(['HomeAddress.label' => 'Home'])
  10. ->setDependent(true);
  11. $this->hasOne('WorkAddress', [
  12. 'className' => 'Addresses'
  13. ])
  14. ->setProperty('work_address')
  15. ->setConditions(['WorkAddress.label' => 'Work'])
  16. ->setDependent(true);
  17. }
  18. }

Note

If you have multiple hasOne associations with the same columns in the conditions, like label, you need to use the table alias before column name as shown above.

Possible keys for hasOne association arrays include:

  • className: the class name of the table being associated to the currentmodel. If you’re defining a ‘User hasOne Address’ relationship, the classNamekey should equal ‘Addresses’.
  • foreignKey: the name of the foreign key found in the other table. This isespecially handy if you need to define multiple hasOne relationships. Thedefault value for this key is the underscored, singular name of the currentmodel, suffixed with ‘_id’. In the example above it would default to‘user_id’.
  • bindingKey: The name of the column in the current table, that will be usedfor matching the foreignKey. If not specified, the primary key (forexample the id column of the Users table) will be used.
  • conditions: an array of find() compatible conditions such as['Addresses.primary' => true]
  • joinType: the type of the join to use in the SQL query, defaultis LEFT. You can use INNER if your hasOne association is always present.
  • dependent: When the dependent key is set to true, and an entity isdeleted, the associated model records are also deleted. In this case we set itto true so that deleting a User will also delete her associated Address.
  • cascadeCallbacks: When this and dependent are true, cascadeddeletes will load and delete entities so that callbacks are properlytriggered. When false, deleteAll() is used to remove associated dataand no callbacks are triggered.
  • propertyName: The property name that should be filled with data from theassociated table into the source table results. By default this is theunderscored & singular name of the association so address in our example.
  • strategy: Defines the query strategy to use. Defaults to ‘join’. The othervalid value is ‘select’, which utilizes a separate query instead.
  • finder: The finder method to use when loading associated records.
    Once this association has been defined, find operations on the Users table cancontain the Address record if it exists:
  1. // In a controller or table method.
  2. $query = $users->find('all')->contain(['Addresses']);
  3. foreach ($query as $user) {
  4. echo $user->address->street;
  5. }

The above would emit SQL that is similar to:

  1. SELECT * FROM users INNER JOIN addresses ON addresses.user_id = users.id;

BelongsTo Associations

Now that we have Address data access from the User table, let’s definea belongsTo association in the Addresses table in order to get access to relatedUser data. The belongsTo association is a natural complement to the hasOne andhasMany associations - it allows us to see related data from the otherdirection.

When keying your database tables for a belongsTo relationship, follow thisconvention:

belongsTo: the current model contains the foreign key.

RelationSchema
Addresses belongsTo Usersaddresses.user_id
Mentors belongsTo Doctorsmentors.doctor_id

Tip

If a Table contains a foreign key, it belongs to the other Table.

We can define the belongsTo association in our Addresses table as follows:

  1. class AddressesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->belongsTo('Users');
  6. }
  7. }

We can also define a more specific relationship using the setters:

  1. class AddressesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. // Prior to 3.4 version, use foreignKey() and joinType()
  6. $this->belongsTo('Users')
  7. ->setForeignKey('user_id')
  8. ->setJoinType('INNER');
  9. }
  10. }

Possible keys for belongsTo association arrays include:

  • className: the class name of the model being associated to the currentmodel. If you’re defining a ‘Profile belongsTo User’ relationship, theclassName key should equal ‘Users’.
  • foreignKey: the name of the foreign key found in the current table. Thisis especially handy if you need to define multiple belongsTo relationships tothe same model. The default value for this key is the underscored, singularname of the other model, suffixed with _id.
  • bindingKey: The name of the column in the other table, that will be usedfor matching the foreignKey. If not specified, the primary key (forexample the id column of the Users table) will be used.
  • conditions: an array of find() compatible conditions or SQL strings suchas ['Users.active' => true]
  • joinType: the type of the join to use in the SQL query, default is LEFTwhich may not fit your needs in all situations, INNER may be helpful when youwant everything from your main and associated models or nothing at all.
  • propertyName: The property name that should be filled with data from theassociated table into the source table results. By default this is theunderscored & singular name of the association so user in our example.
  • strategy: Defines the query strategy to use. Defaults to ‘join’. The othervalid value is ‘select’, which utilizes a separate query instead.
  • finder: The finder method to use when loading associated records.
    Once this association has been defined, find operations on the Addresses table cancontain the User record if it exists:
  1. // In a controller or table method.
  2. $query = $addresses->find('all')->contain(['Users']);
  3. foreach ($query as $address) {
  4. echo $address->user->username;
  5. }

The above would emit SQL that is similar to:

  1. SELECT * FROM addresses LEFT JOIN users ON addresses.user_id = users.id;

HasMany Associations

An example of a hasMany association is “Article hasMany Comments”. Defining thisassociation will allow us to fetch an article’s comments when the article isloaded.

When creating your database tables for a hasMany relationship, follow thisconvention:

hasMany: the other model contains the foreign key.

RelationSchema
Article hasMany CommentComment.article_id
Product hasMany OptionOption.product_id
Doctor hasMany PatientPatient.doctor_id

We can define the hasMany association in our Articles model as follows:

  1. class ArticlesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasMany('Comments');
  6. }
  7. }

We can also define a more specific relationship using the setters:

  1. class ArticlesTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->hasMany('Comments')
  6. ->setForeignKey('article_id')
  7. ->setDependent(true);
  8. }
  9. }

Sometimes you may want to configure composite keys in your associations:

  1. // Within ArticlesTable::initialize() call
  2. $this->hasMany('Reviews')
  3. ->setForeignKey([
  4. 'article_id',
  5. 'article_hash'
  6. ]);

Relying on the example above, we have passed an array containing the desiredcomposite keys to setForeignKey(). By default the bindingKey would beautomatically defined as id and hash respectively, but let’s assume thatyou need to specify different binding fields than the defaults, you can setup itmanually with setBindingKey():

  1. // Within ArticlesTable::initialize() call
  2. $this->hasMany('Reviews')
  3. ->setForeignKey([
  4. 'article_id',
  5. 'article_hash'
  6. ])
  7. ->setBindingKey([
  8. 'whatever_id',
  9. 'whatever_hash'
  10. ]);

It is important to note that foreignKey values refers to the reviewstable and bindingKey values refers to the articles table.

Possible keys for hasMany association arrays include:

  • className: the class name of the model being associated tothe current model. If you’re defining a ‘User hasMany Comment’relationship, the className key should equal ‘Comments’.
  • foreignKey: the name of the foreign key found in the othertable. This is especially handy if you need to define multiplehasMany relationships. The default value for this key is theunderscored, singular name of the actual model, suffixed with‘_id’.
  • bindingKey: The name of the column in the current table, that will be usedfor matching the foreignKey. If not specified, the primary key (forexample the id column of the Articles table) will be used.
  • conditions: an array of find() compatible conditions or SQLstrings such as ['Comments.visible' => true]
  • sort: an array of find() compatible order clauses or SQLstrings such as ['Comments.created' => 'ASC']
  • dependent: When dependent is set to true, recursive modeldeletion is possible. In this example, Comment records will bedeleted when their associated Article record has been deleted.
  • cascadeCallbacks: When this and dependent are true, cascadeddeletes will load and delete entities so that callbacks are properlytriggered. When false, deleteAll() is used to remove associated dataand no callbacks are triggered.
  • propertyName: The property name that should be filled with data from theassociated table into the source table results. By default this is theunderscored & plural name of the association so comments in our example.
  • strategy: Defines the query strategy to use. Defaults to ‘select’. Theother valid value is ‘subquery’, which replaces the IN list with anequivalent subquery.
  • saveStrategy: Either ‘append’ or ‘replace’. Defaults to ‘append’. When ‘append’ the currentrecords are appended to any records in the database. When ‘replace’ associatedrecords not in the current set will be removed. If the foreign key is a nullablecolumn or if dependent is true records will be orphaned.
  • finder: The finder method to use when loading associated records.
    Once this association has been defined, find operations on the Articles tablecan contain the Comment records if they exist:
  1. // In a controller or table method.
  2. $query = $articles->find('all')->contain(['Comments']);
  3. foreach ($query as $article) {
  4. echo $article->comments[0]->text;
  5. }

The above would emit SQL that is similar to:

  1. SELECT * FROM articles;
  2. SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);

When the subquery strategy is used, SQL similar to the following will begenerated:

  1. SELECT * FROM articles;
  2. SELECT * FROM comments WHERE article_id IN (SELECT id FROM articles);

You may want to cache the counts for your hasMany associations. This is usefulwhen you often need to show the number of associated records, but don’t want toload all the records just to count them. For example, the comment count on anygiven article is often cached to make generating lists of articles moreefficient. You can use the CounterCacheBehavior to cache counts of associated records.

You should make sure that your database tables do not contain columns that matchassociation property names. If for example you have counter fields that conflictwith association properties, you must either rename the association property, orthe column name.

BelongsToMany Associations

Note

In 3.0 and onward hasAndBelongsToMany / HABTM has been renamed tobelongsToMany / BTM.

An example of a BelongsToMany association is “Article BelongsToMany Tags”, wherethe tags from one article are shared with other articles. BelongsToMany isoften referred to as “has and belongs to many”, and is a classic “many to many”association.

The main difference between hasMany and BelongsToMany is that the link betweenthe models in a BelongsToMany association is not exclusive. For example, we arejoining our Articles table with a Tags table. Using ‘funny’ as a Tag for myArticle, doesn’t “use up” the tag. I can also use it on the next articleI write.

Three database tables are required for a BelongsToMany association. In theexample above we would need tables for articles, tags andarticles_tags. The articles_tags table contains the data that linkstags and articles together. The joining table is named after the two tablesinvolved, separated with an underscore by convention. In its simplest form, thistable consists of article_id and tag_id.

belongsToMany requires a separate join table that includes both _model_names.

RelationshipJoin Table Fields
Article belongsToMany Tagarticles_tags.id, articles_tags.tag_id, articles_tags.article_id
Patient belongsToMany Doctordoctors_patients.id, doctors_patients.doctor_id,doctors_patients.patient_id.

We can define the belongsToMany association in both our models as follows:

  1. // In src/Model/Table/ArticlesTable.php
  2. class ArticlesTable extends Table
  3. {
  4. public function initialize(array $config)
  5. {
  6. $this->belongsToMany('Tags');
  7. }
  8. }
  9.  
  10. // In src/Model/Table/TagsTable.php
  11. class TagsTable extends Table
  12. {
  13. public function initialize(array $config)
  14. {
  15. $this->belongsToMany('Articles');
  16. }
  17. }

We can also define a more specific relationship using configuration:

  1. // In src/Model/Table/TagsTable.php
  2. class TagsTable extends Table
  3. {
  4. public function initialize(array $config)
  5. {
  6. $this->belongsToMany('Articles', [
  7. 'joinTable' => 'articles_tags',
  8. ]);
  9. }
  10. }

Possible keys for belongsToMany association arrays include:

  • className: the class name of the model being associated tothe current model. If you’re defining a ‘Article belongsToMany Tag’relationship, the className key should equal ‘Tags’.
  • joinTable: The name of the join table used in thisassociation (if the current table doesn’t adhere to the namingconvention for belongsToMany join tables). By default this tablename will be used to load the Table instance for the join table.
  • foreignKey: The name of the foreign key that references the current modelfound on the join table, or list in case of composite foreign keys.This is especially handy if you need to define multiplebelongsToMany relationships. The default value for this key is theunderscored, singular name of the current model, suffixed with ‘_id’.
  • bindingKey: The name of the column in the current table, that will be usedfor matching the foreignKey. Defaults to the primary key.
  • targetForeignKey: The name of the foreign key that references the targetmodel found on the join model, or list in case of composite foreign keys.The default value for this key is the underscored, singular name ofthe target model, suffixed with ‘_id’.
  • conditions: an array of find() compatible conditions. If you haveconditions on an associated table, you should use a ‘through’ model, anddefine the necessary belongsTo associations on it.
  • sort: an array of find() compatible order clauses.
  • dependent: When the dependent key is set to false, and an entity isdeleted, the data of the join table will not be deleted.
  • through: Allows you to provide either the alias of the Table instance youwant used on the join table, or the instance itself. This makes customizingthe join table keys possible, and allows you to customize the behavior of thepivot table.
  • cascadeCallbacks: When this is true, cascaded deletes will load anddelete entities so that callbacks are properly triggered on join tablerecords. When false, deleteAll() is used to remove associated data andno callbacks are triggered. This defaults to false to help reduceoverhead.
  • propertyName: The property name that should be filled with data from theassociated table into the source table results. By default this is theunderscored & plural name of the association, so tags in our example.
  • strategy: Defines the query strategy to use. Defaults to ‘select’. Theother valid value is ‘subquery’, which replaces the IN list with anequivalent subquery.
  • saveStrategy: Either ‘append’ or ‘replace’. Defaults to ‘replace’.Indicates the mode to be used for saving associated entities. The former willonly create new links between both side of the relation and the latter willdo a wipe and replace to create the links between the passed entities whensaving.
  • finder: The finder method to use when loading associated records.
    Once this association has been defined, find operations on the Articles table cancontain the Tag records if they exist:
  1. // In a controller or table method.
  2. $query = $articles->find('all')->contain(['Tags']);
  3. foreach ($query as $article) {
  4. echo $article->tags[0]->text;
  5. }

The above would emit SQL that is similar to:

  1. SELECT * FROM articles;
  2. SELECT * FROM tags
  3. INNER JOIN articles_tags ON (
  4. tags.id = article_tags.tag_id
  5. AND article_id IN (1, 2, 3, 4, 5)
  6. );

When the subquery strategy is used, SQL similar to the following will begenerated:

  1. SELECT * FROM articles;
  2. SELECT * FROM tags
  3. INNER JOIN articles_tags ON (
  4. tags.id = article_tags.tag_id
  5. AND article_id IN (SELECT id FROM articles)
  6. );

Using the ‘through’ Option

If you plan on adding extra information to the join/pivot table, or if you needto use join columns outside of the conventions, you will need to define thethrough option. The through option provides you full control over howthe belongsToMany association will be created.

It is sometimes desirable to store additional data with a many to manyassociation. Consider the following:

  1. Student BelongsToMany Course
  2. Course BelongsToMany Student

A Student can take many Courses and a Course can be taken by many Students. Thisis a simple many to many association. The following table would suffice:

  1. id | student_id | course_id

Now what if we want to store the number of days that were attended by thestudent on the course and their final grade? The table we’d want would be:

  1. id | student_id | course_id | days_attended | grade

The way to implement our requirement is to use a join model, otherwise knownas a hasMany through association. That is, the association is a modelitself. So, we can create a new model CoursesMemberships. Take a look at thefollowing models:

  1. class StudentsTable extends Table
  2. {
  3. public function initialize(array $config)
  4. {
  5. $this->belongsToMany('Courses', [
  6. 'through' => 'CoursesMemberships',
  7. ]);
  8. }
  9. }
  10.  
  11. class CoursesTable extends Table
  12. {
  13. public function initialize(array $config)
  14. {
  15. $this->belongsToMany('Students', [
  16. 'through' => 'CoursesMemberships',
  17. ]);
  18. }
  19. }
  20.  
  21. class CoursesMembershipsTable extends Table
  22. {
  23. public function initialize(array $config)
  24. {
  25. $this->belongsTo('Students');
  26. $this->belongsTo('Courses');
  27. }
  28. }

The CoursesMemberships join table uniquely identifies a given Student’sparticipation on a Course in addition to extra meta-information.

Default Association Conditions

The finder option allows you to use a custom finder to load associated record data. This lets you encapsulateyour queries better and keep your code DRY’er. There are some limitations whenusing finders to load data in associations that are loaded using joins(belongsTo/hasOne). Only the following aspects of the query will be applied tothe root query:

  • WHERE conditions.
  • Additional joins.
  • Contained associations.
    Other aspects of the query, such as selected columns, order, group by, havingand other sub-statements, will not be applied to the root query. Associationsthat are not loaded through joins (hasMany/belongsToMany), do not have theabove restrictions and can also use result formatters or map/reduce functions.

Loading Associations

Once you’ve defined your associations you can eager load associations when fetching results.