CakePHP 3.4: query where or matching - cakephp

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

Related

CakePHP 3 hasMany association with bitwise expression

I have two tables, AclGroups and AclPermissions, and I want to create a hasMany relationship between them, i.e AclGroups has many AclPermissions.
The condition to determine whether a group owns a given permission is done in a single bitwise check. This is what i'm trying to do:
SELECT
*
FROM
acl_groups
JOIN
acl_permissions ON acl_permissions.permission & acl_groups.permission != 0
In AclGroupsTable I have tried the following:
$this->hasMany('AclPermissions', [
'className' => 'AclPermissions',
'foreignKey' => 'permission',
'conditions' => [
'AclPermissions.permission & AclGroups.permission !=' => 0
]
]);
But that just gives me
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'aclgroups.permission' in 'where clause'
In my controller I do:
$this->AclGroups->find('all')->contain(['AclPermissions']);
I suppose the real question is: Is there a way I can change the conditions of the ON clause in the query that fetches associated records
As mentioned in the comments, records of hasMany associations (and belongsToMany for that matter) will always be retrieved in a separate query when using contain().
If you need to create joins with such an association, then you must explicitly use the corresponding functionality for joins, for example leftJoinWith():
$this->AclGroups->find('all')->leftJoinWith('AclPermissions');
This will create a query similar to the one you are showing. However it would also generate the default conditions using the configured foreign key, you'd have to disable the foreign key in order to avoid that, like:
$this->hasMany('AclPermissions', [
'foreignKey' => false, // << like this
// ...
]);
Given that the association conditions won't work with contain() (and disabling the foreign key will make it even more unusable for that purpose), you may want to create a separate association for your joining purposes, or use the "lower level" join methods, where you specify all the conditions manually (you can for example put this in a custom finder in order to keep your code DRY):
$query = $this->AclGroups
->find('all')
->join([
'table' => 'acl_permissions',
'alias' => 'AclPermissions',
'type' => 'LEFT',
'conditions' => [
'AclPermissions.permission & AclGroups.permission != :permission'
]
])
->bind(':permission', 0, 'integer');
Note that the value is being explicitly bound here to ensure that the correct type is being used, as it couldn't be determined from the non-standard left hand value (which isn't really ment to contain SQL snippets - you may want want to look into using expressions).
See also
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Adding Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

How to determine if a DBIx::Class::Row has a relationship already fetched?

Here's my situation: we have master tables with relationships to attribute tables. Sometimes, we fetch a row all by itself:
my $row = $rs->search({ some_key => 'some_value' })->first;
and sometimes we join one or more tables:
my $row = $rs->search({ some_key => 'some_value' }, { join => 'attributes' });
We have "helper" methods that look up specific attributes:
sub get_x_attr {
my $obj = shift;
my $x_attr = $obj->attributes->search({ attribute_name => 'x' })->one_row;
return $x_attr ? $x_attr->attribute_value : 'default';
}
This seems to issue another query, and while it's a pretty low-impact query, these add up when you are doing that zillions of times a day.
Now, if the row was joined originally, I could write the helper as:
my #attrs = grep { $_->attribute_name eq 'x' } $obj->attributes->all;
my $x_attribute = $attrs[0] || return 'default';
# etc.
and there'd be no additional query.
Here's my question: is there a safe, reliable way to interrogate "$obj" to see if it's got attributes pre-fetched? And further, is there any way to tell after the fact if the join was conditional (e.g., 'WHERE attribute_name = 'some_other_value', which would make $obj->attributes rather useless here)?
(I did some digging, and found that $obj->{internals}{related_resultsets} has the answer to the first question, but since it's not part of the exposed API, I'm very much opposed to using it this way.)
Use the relationship methods, if the relationship is prefetched the cached related row is used, if not a query is automatically executed.
Note that joining won't populate the cache, only prefetch does.
Search will always issue a query and never restrict the resultset via Perl code like your grep example.

CakePHP 3 - Count many-to-many Association

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

Cakephp contains association when using matching()

In my project I have the following tables: Messages, Recipients, Groups and Users. A Message has many Recipients, and a Recipient has one Group and one User.
In my RecipientsTable::beforeFind I have some code to automatically contain Groups and Users for Recipient finds, since I always need to access those associations.
public function beforeFind($event, $query, $options, $primary) {
return $query->contain([
'Groups',
'Users',
]);
}
I don't know if this is a bad design decision but it has worked for me so far.
The problem has come now that I'm trying to filter messages by group, and I tried doing so by using the matching function:
$possible_groups = [1,2,3]; //just an example
$query->matching('Recipients', function($q) use ($possible_groups){
return $q->where(['Recipients.group_id IN' => $possible_groups]);
});
When I execute the query, I get the following error:
Messages is not associated with Groups
Is there any way to keep my beforeFind like that and be able to use matching? Or, is there a better way to automatically load associations without using beforeFind?
TL;DR: A hasMany B, B hasOne C. If a query on table A uses matching on table B and table B's beforeFind uses contain to load C, C ends up contained onto the original query (of A) and the execution fails since A is not associated with C.
Use table classes to load Table associations automatically. Like using hasMany(), hasOne(), belongsTo(), belongsToMany() in necessary table classes.
Official documentation:
https://book.cakephp.org/3.0/en/orm/associations.html
Table files should be in the Table folder inside the Model folder like src\Model\Table.

How do I join two tables in a third n..n (hasAndBelongsToMany) relationship in CakePHP?

I have a n...n structure for two tables, makes and models. So far no problem.
In a third table (products) like:
id
make_id
model_id
...
My problem is creating a view for products of one specifi make inside my ProductsController containing just that's make models:
I thought this could work:
var $uses = array('Make', 'Model');
$this->Make->id = 5; // My Make
$this->Make->find(); // Returns only the make I want with it's Models (HABTM)
$this->Model->find('list'); // Returns ALL models
$this->Make->Model->find('list'); // Returns ALL models
So, If I want to use the list to pass to my view to create radio buttons I will have to do a foreach() in my make array to find all models titles and create a new array and send to the view via $this->set().
$makeArray = $this->Make->find();
foreach ($makeArray['Model'] as $model) {
$modelList[] = $model['title'];
}
$this->set('models', $models)
Is there any easier way to get that list without stressing the make Array. It will be a commom task to develops such scenarios in my application(s).
Thanks in advance for any hint!
Here's my hint: Try getting your query written in regular SQL before trying to reconstruct using the Cake library. In essence you're doing a lot of extra work that the DB can do for you.
Your approach (just for show - not good SQL):
SELECT * FROM makes, models, products WHERE make_id = 5
You're not taking into consideration the relationships (unless Cake auto-magically understands the relationships of the tables)
You're probably looking for something that joins these things together:
SELECT models.title FROM models
INNER JOIN products
ON products.model_id = models.model_id
AND products.make_id = 5
Hopefully this is a nudge in the right direction?
Judging from your comment, what you're asking for is how to get results from a certain model, where the condition is in a HABTM related model. I.e. something you'd usually do with a JOIN statement in raw SQL.
Currently that's one of the few weak points of Cake. There are different strategies to deal with that.
Have the related model B return all ids of possible candidates for Model A, then do a second query on Model A. I.e.:
$this->ModelB->find('first', array('conditions' => array('field' => $condition)));
array(
['ModelB'] => array( ... ),
['ModelA'] => array(
[0] => array(
'id' => 1
)
)
Now you have an array of all ids of ModelA that belong to ModelB that matches your conditions, which you can easily extract using Set::extract(). Basically the equivalent of SELECT model_a.id FROM model_b JOIN model_a WHERE model_b.field = xxx. Next you look for ModelA:
$this->ModelA->find('all', array('conditions' => array('id' => $model_a_ids)));
That will produce SELECT model_a.* FROM model_a WHERE id IN (1, 2, 3), which is a roundabout way of doing the JOIN statement. If you need conditions on more than one related model, repeat until you have all the ids for ModelA, SQL will use the intersection of all ids (WHERE id IN (1, 2, 3) AND id IN (3, 4, 5)).
If you only need one condition on ModelB but want to retrieve ModelA, just search for ModelB. Cake will automatically retrieve related ModelAs for you (see above). You might need to Set::extract() them again, but that might already be sufficient.
You can use the above method and combine it with the Containable behaviour to get more control over the results.
If all else fails or the above methods simply produce too much overhead, you can still write your own raw SQL with $this->Model->query(). If you stick to the Cake SQL standards (naming tables correctly with FROM model_as AS ModelA) Cake will still post-process your results correctly.
Hope this sends you in the right direction.
All your different Make->find() and Model->find() calls are completely independent of each other. Even Make->Model->find() is the same as Model->find(), Cake does not in any way remember or take into account what you have already found in other models. What you're looking for is something like:
$this->Product->find('all', array('conditions' => array('make_id' => 5)));
Check out the Set::extract() method for getting a list of model titles from the results of $this->Make->find()
The solution can be achieved with the use of the with operation in habtm array on the model.
Using with you can define the "middle" table like:
$habtm = " ...
'with' => 'MakeModel',
... ";
And internally, in the Model or Controller, you can issue conditions to the find method.
See: http://www.cricava.com/blogs/index.php?blog=6&title=modelizing_habtm_join_tables_in_cakephp_&more=1&c=1&tb=1&pb=1

Resources