I have a database with Rounds and Users. Rounds belongsToMany Users and Users belongsToMany Rounds, so a many-to-many relation. A join table rounds_users was added to do this.
EDIT: Used the incorrect phrase here. I meant 'belongsToMany' instead of 'hasMany'
Now I want to retrieve a list of Rounds, together with the number of linked Users per round.
In case of a one-to-many relation something like the following would work:
$rounds = $this->Rounds->find()
->contain(['Users' => function ($q)
{
return $q->select(['Users.id', 'number' => 'COUNT(Users.round_id)'])
->group(['Users.round_id']);
}
]);
...According to Count in contain Cakephp 3
However, in a many-to-many relation Users.round_id does not exist. So, what could work instead?
Note: Several questions like this have been asked already, but few about CakePHP 3.x, so I still wanted to give this a try.
Note 2: I can get around this by using the PHP function count, though I'd rather do it more elegantly
EDIT: As suggested below, manually joining seems to do the trick:
$rounds_new = $this->Rounds->find()
->select($this->Rounds)
->select(['user_count' => 'COUNT(Rounds.id)'])
->leftJoinWith('Users')
->group('Rounds.id');
...With one problem! Rounds without Users still get a user_count equal to 1. What could be the problem?
Join in the association
As already mentioned in the comments, you can always join in associations instead of containing them (kinda like in your SUM() on ManyToMany question).
The reason why you retrieve a count of 1 for rounds that do not have any associated users, is that you are counting on the wrong table. Counting on Rounds will of course result in a count of at least 1, as there will always be at least 1 round, the round for which no associated users exist.
So long story short, count on Users instead:
$rounds = $this->Rounds
->find()
->select($this->Rounds)
->select(function (\Cake\ORM\Query $query) {
return [
'user_count' => $query->func()->count('Users.id')
];
})
->leftJoinWith('Users')
->group('Rounds.id');
Counter cache for belongsToMany associations
There's also the counter cache behavior, it works for belongsToMany associations too, as long as they are set up to use a concrete join table class, which can be configured via the association configurations through option:
class RoundsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Users', [
'through' => 'RoundsUsers'
]);
}
}
class UsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Rounds', [
'through' => 'RoundsUsers'
]);
}
}
The counter cache would be set up in the join table class, in this example RoundsUsersTable, which would have two belongsTo associations, one to Rounds and one to Users:
class RoundsUsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Rounds');
$this->belongsTo('Users');
$this->addBehavior('CounterCache', [
'Rounds' => ['user_count']
]);
}
}
Count in containment
If you would have actually explicitly created hasMany associations (instead of belongsToMany), then you would have a one Rounds to many RoundsUsers relation, ie you could still use the linked "count in contain" example via RoundsUsers.
However, that would leave you with a structure where the count would be placed in a nested entity, which in turn would be missing for rounds that do not have any associated users. I would imagine that in most situations this kind of structure would require reformatting, so it's probably not the best solution. However, for the sake of completion, here's an example:
$rounds = $this->Rounds
->find()
->contain(['RoundsUsers' => function (\Cake\ORM\Query $query) {
return $query
->select(function (\Cake\ORM\Query $query) {
return [
'RoundsUsers.round_id',
'number' => $query->func()->count('RoundsUsers.round_id')
];
})
->group(['RoundsUsers.round_id']);
}]
);
See also
Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Cookbook > Database Access & ORM > Query Builder > Returning the Total Count of Records
Cookbook > Database Access & ORM > Behaviors > CounterCache
Cookbook > Database Access & ORM > Associations - Linking Tables Together > BelongsToMany Associations > Using the ‘through’ Option
Related
i have 3 tables with one-to-one relationship. The phone table has one to one relationship with Model table and Model table has a one to one relationship with Manufacturer table.
phone_table
id
imei
image
model_id
model_table
id
name
image
manufracturer_id
manufracturer_table
id
name
logo
how to get a result like this :-
App\Phone{
imei : "356554512522148",
model : "Galaxy S-10",
manufracturer : "Samsung",
}
I would never throw it into the same array / object, i would firstly do that on transformation. If you use default Laravel transformation you can use getters for it. Simple example on how to access these fields into the same context would be.
$phone = Phone::with('model.manufactor')->find(1);
With secures the queries are optimal for accessing it. How to get data into same layer.
[
'imei' => $phone->imei,
'model' => $phone->model->name,
'manufactor' => $phone->model->manufactor->name,
]
For this to work, you need relations in your model too.
Phone.php
public function model()
{
return $this->belongsTo(Model::class);
}
Model.php
public function manufactor()
{
return $this->belongsTo(Manufactor::class);
}
Just join them:
\App\Phone::leftjoin('model_table AS mo', 'mo.id', '=','phone_table.model_id')
->leftjoin('manufracturer_table AS ma', 'ma.id', '='. 'mo.manufracturer_id')
->selectRaw('phone_table.imei, mo.name AS model, ma.name AS manufracturer')
->first()
And sometimes you need to think about why you want to split table to one-to-one relationship.
Is there a table not usually be used, or one of them need to be connected by another tables. is this just for saving space or reduce IO cost.
If there are not any other reason and you always need to get these tables' information, maybe you can merge to one table.
I have a problem with declaring conditions for my BTM association - probably missunderstood something.
Imagine a few tables - Notes, NotesEntities, Entities (the last one is not an actual table, but can be any table like Products, Customers, Services, etc...)
In NotesTable there is "entity" field in which there are values like "Services", "Products", etc...
I have se an association in NotesTable like this assuming it will get rows from ServicesTable only if "entity" field in NotesTable = "Services":
$this->belongsToMany('Services', [
'foreignKey' => 'note_id',
'targetForeignKey' => 'entity_id',
'joinTable' => 'notes_entities',
'conditions' => ['Notes.entity' => 'Services']
]);
But the condition doesn't work and so if I want to fetch a note, where entity = "Customers", it fetches also Services where id of a service = wanted id of a customer. For example: I fetch a Note which has entity = "Customers" and is connected to Customers with ids [1, 2]. That works fine. But when I contain Services, it also fetches Services with ids [1, 2] instead of leaving "services" array empty unless entity = "Services".
Is there a way to achieve such association? Is it possible to set such global conditions in CakePHP 3? I believe I'm not the only one who can use such thing. What am I missing?
EDIT 1:
The other way around it works. If I fetch a customer and contain Notes, everything is fine and it fetches only notes with entity = "Customers".
I have a belongsToMany association on Users and Contacts.
I would like to find the Contacts of the given User.
I would need something like
$this->Contacts->find()->contain(['Users' => ['Users.id' => 1]]);
The cookbook speaks about giving conditions to contain, custom finder methods and sing through association key, but I did not find out how to put these together.
Use Query::matching() or Query::innerJoinWith()
When querying from the Contacts table, then what you are looking for is Query::matching() or Query::innerJoinWith(), not (only) Query::contain().
Note that innerJoinWith() is usually preferred in order to avoid problems with strict grouping, as matching() will add the fields of the association to the select list, which can cause problems as they are usually not functionally dependent.
See Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Here's an example using your tables:
$this->Contacts
->find()
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
This will automatically add the required joins + conditions to the generated query, so that only those contacts are being retrieved that are associated to at least one user with the id 1.
Target the join table
In case you'd have a manually set up many to many association via hasMany and belongsTo, you can directly target the join table:
$this->Contacts
->find()
->innerJoinWith('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.user_id' => 1]);
});
Including containments
In case you actually want to have all the associations returned in your results too, then just keep using contain() too:
$this->Contacts
->find()
->contain('Users')
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
That would contain all users that belong to a contact.
Restricting containments
In cases where you have multiple matches, and you'd wanted to contain only those matches, you'd have to filter the containment too. In this example it doesn't make much sense since there would be only one match, but in other situations it might be useful, say for example if you'd wanted to match all contacts that have active users, and retrieve the contacts including only the active associated users:
$this->Contacts
->find()
->contain(['Users' => function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
}])
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
})
->group('Contacts.id');
Given that there could be duplicates, ie multiple active users for a single contact, you'll probably want to group things accordingly, in order to avoid retrieving duplicate contact records.
Deep associations
You can also target deeper associations that way, by using the dot notated path syntax known from Query::contain(). Say for example you had a Users hasOne Profiles association, and you want to match only on those users that want to receive notifications, that could look something like this:
->innerJoinWith('Users.Profiles', function(\Cake\ORM\Query $q) {
return $q->where(['Profiles.receive_notifications' => true]);
})
This will automatically create all the required additional joins.
Select from the other table instead
With these associations and your simple requirements, you could also easily query from the other side, ie via the Users table and use just Query::contain() to include the associated contacts, like
$this->Users
->find()
->contain('Contacts')
->where([
'Users.id' => 1
])
->first();
All the contacts can then be found in the entities contacts property.
While #ndm answer got me where I needed to with a similar issue, solution needed a little tweak.
Problem was getting users filtered by data on the joinTable adding matching didn't return the users. So this is what I ended up with, hope it'll help someone.
$this->Contacts
->find()
->contain('Users', function(\Cake\ORM\Query $q) {
return $q->matching('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.some_field' => 1]);
}
});
This got me Contacts with Users who has some_field set to 1 in association table. Or am I overcomplicating and there is a better solution?
I read the cookbook, but I can not figure out how to combine in a single query a matching() and a orWhere().
Example: I have Photo that belongs from Album. Both have the active field. So I'm trying to write a findInactive() method. A "inactive" photo has the active field as false or matching an album that has the active fields as false.
Something like this:
public function findInactive(Query $query, array $options)
{
$query->matching('Albums', function ($q) {
return $q->where(['Albums.active' => false]);
})
->orWhere(['Photos.active' => false])
->enableAutoFields(true);
return $query;
}
But that does not work:
'SELECT [...] FROM photos Photos INNER JOIN photos_albums Albums ON (Albums.active = :c0 AND Albums.id = (Photos.album_id)) WHERE Photos.active = :c1'
How to do? Thanks.
EDIT
Maybe a possible solution is usecontain():
$query->contain(['Albums => ['fields' => ['active']]])
->where(['Photos.active' => false])
->orWhere(['Albums.active' => false]);
But is it not possible to use matching() or innerJoinWith()?
Add the conditions to the main query
matching() or innerJoinWith() with conditions is the wrong way to go, as the conditions are being addded to the INNER joins ON clause, which will cause the Photo row to be excluded in case the Albums.active condition doesn't match.
If you want to only receive photos that belong to an album, then you want to use matching() or innerJoinWith(), but you'll have to add the conditions to the main query instead, ie:
$query
->innerJoinWith('Albums')
->where(['Albums.active' => false])
->orWhere(['Photos.active' => false])
// ...
In case a photo doesn't have to belong to an album, or it's not important whether it does, you could use either leftJoin(), leftJoinWith(), or even contain().
The latter however may use the INNER joinStrategy and/or the select strategy (which uses a separate query), so you'd need to take care of ensuring that it uses LEFT and join instead. Using containments however is usually only advised if you actually want to contain something, and given that your finder seems to be supposed to just filter things, I'd say go with leftJoinWith() instead:
$query
->leftJoinWith('Albums')
->where(['Albums.active' => false])
->orWhere(['Photos.active' => false])
// ...
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data Via Matching And Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Retrieving Associated Data
Cookbook > Database Access & ORM > Associations - Linking Tables Together > BelongsTo Associations
I have a belongsToMany association on Users and Contacts.
I would like to find the Contacts of the given User.
I would need something like
$this->Contacts->find()->contain(['Users' => ['Users.id' => 1]]);
The cookbook speaks about giving conditions to contain, custom finder methods and sing through association key, but I did not find out how to put these together.
Use Query::matching() or Query::innerJoinWith()
When querying from the Contacts table, then what you are looking for is Query::matching() or Query::innerJoinWith(), not (only) Query::contain().
Note that innerJoinWith() is usually preferred in order to avoid problems with strict grouping, as matching() will add the fields of the association to the select list, which can cause problems as they are usually not functionally dependent.
See Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Here's an example using your tables:
$this->Contacts
->find()
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
This will automatically add the required joins + conditions to the generated query, so that only those contacts are being retrieved that are associated to at least one user with the id 1.
Target the join table
In case you'd have a manually set up many to many association via hasMany and belongsTo, you can directly target the join table:
$this->Contacts
->find()
->innerJoinWith('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.user_id' => 1]);
});
Including containments
In case you actually want to have all the associations returned in your results too, then just keep using contain() too:
$this->Contacts
->find()
->contain('Users')
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
That would contain all users that belong to a contact.
Restricting containments
In cases where you have multiple matches, and you'd wanted to contain only those matches, you'd have to filter the containment too. In this example it doesn't make much sense since there would be only one match, but in other situations it might be useful, say for example if you'd wanted to match all contacts that have active users, and retrieve the contacts including only the active associated users:
$this->Contacts
->find()
->contain(['Users' => function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
}])
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
})
->group('Contacts.id');
Given that there could be duplicates, ie multiple active users for a single contact, you'll probably want to group things accordingly, in order to avoid retrieving duplicate contact records.
Deep associations
You can also target deeper associations that way, by using the dot notated path syntax known from Query::contain(). Say for example you had a Users hasOne Profiles association, and you want to match only on those users that want to receive notifications, that could look something like this:
->innerJoinWith('Users.Profiles', function(\Cake\ORM\Query $q) {
return $q->where(['Profiles.receive_notifications' => true]);
})
This will automatically create all the required additional joins.
Select from the other table instead
With these associations and your simple requirements, you could also easily query from the other side, ie via the Users table and use just Query::contain() to include the associated contacts, like
$this->Users
->find()
->contain('Contacts')
->where([
'Users.id' => 1
])
->first();
All the contacts can then be found in the entities contacts property.
While #ndm answer got me where I needed to with a similar issue, solution needed a little tweak.
Problem was getting users filtered by data on the joinTable adding matching didn't return the users. So this is what I ended up with, hope it'll help someone.
$this->Contacts
->find()
->contain('Users', function(\Cake\ORM\Query $q) {
return $q->matching('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.some_field' => 1]);
}
});
This got me Contacts with Users who has some_field set to 1 in association table. Or am I overcomplicating and there is a better solution?