Condition for related table ORM Kohana - php

For example I have 3 tables:
songs(id, song_name)
song_category(id, song_id, category_id)
categories(id, name)
I want to get songs which have categories with id higher than 5. I want to do it using ORM, not with simple SQL query. Is it possible to do it with one query like this:
$songs = ORM::factory("songs")->where("category.id > 5")

No, you cannot do this with a single Kohana ORM call.
The best way I have found to do it is something like this, which makes a modification to the SQL query that the ORM will generate:
// Get the basic "song" model
$songs = ORM::factory("songs");
// Get the information about how it is connected to
// the "category" model using the `through` model
$song_relations = $results->has_many();
$category_relation = $song_relations['categories'];
$through = $category_relation['through'];
// Join on `through` model's target foreign key (far_key) and `target` model's primary key
$join_col1 = $through.'.'.$category_relation['foreign_key'];
$join_col2 = $songs->object_name().'.'.$songs->primary_key();
$songs->join($through)->on($join_col1, '=', $join_col2);
// Now, filter on the
$songs->where($through.'.'.$category_relation['far_key'], '>', 5);
$arr = $results->find_all()->as_array();
You could save some code by hardcoding the values in the join method call, but this way leverages the ORM relation definitions that you already have.
This assumes that your Song model has the following code in it:
protected $_has_many = [
'categories' => [
'model' => 'category',
'through' => 'song_category',
'foreign_key' => 'song_id',
'far_key' => 'category_id',
]
];

Related

CakePHP query returning different results then running directly in sql

GOAL
I am attempting to return all features and all associated (if any) features_user_types where the user_type_id = ?.
So for example, I have 2 features. I want both to be returned along with all associated features_user_types as long as the user_type_id = 2. If there is no matching feature_user_type then it should return feature anyway.
EXPECTED RESULTS
Example Output: WHERE user_type_id = 2
"features": [
{
"id": 1,
"features_user_types": [
{
"id": 79,
"feature_id": 1,
"user_type_id": 2,
"position": 3
}
]
},
{
"id": 2,
"features_user_types": []
}
]
ACTUAL RESULTS
However, currently it is returning all associated features_user_types despite their id not equaling 2.
$query->toArray() Output:
"features": [
{
"id": 1,
"features_user_types": [
{
"id": 79,
"feature_id": 1,
"user_type_id": 2,
"position": 3
}
]
},
{
"id": 2,
"features_user_types": [
{
"id": 72,
"feature_id": 2,
"user_type_id": 1,
"position": 9
}
]
}
]
DATA STRUCTURE
Table Structure:
features
-id
features_user_types
-id
-feature_id
-user_type_id
-position
user_types
-id
CakePHP Association Definitions:
FeaturesTable:
$this->belongsToMany('UserTypes', [
'foreignKey' => 'feature_id',
'targetForeignKey' => 'user_type_id',
'joinTable' => 'features_user_types'
]);
$this->hasMany('FeaturesUserTypes', [
'foreignKey' => 'feature_id'
]);
UserTypesTable:
$this->belongsToMany('Features', [
'foreignKey' => 'user_type_id',
'targetForeignKey' => 'feature_id',
'joinTable' => 'features_user_types'
]);
$this->hasMany('FeaturesUserTypes', [
'className' => 'FeaturesUserTypes',
'foreignKey' => 'user_type_id'
]);
FeaturesUserTypesTable:
$this->belongsTo('Features', [
'foreignKey' => 'feature_id',
'joinType' => 'INNER'
]);
$this->belongsTo('UserTypes', [
'foreignKey' => 'user_type_id',
'joinType' => 'INNER'
]);
QUERY OBJECT
I have a query builder in my cakephp app that is creating the following sql according to the $query->sql():
SELECT DISTINCT
Features.id AS `Features__id`,
Features.created AS `Features__created`,
Features.modified AS `Features__modified`,
Features.name AS `Features__name`,
Features.description AS `Features__description`
FROM features Features
LEFT JOIN features_user_types FeaturesUserTypes
ON (FeaturesUserTypes.user_type_id = 2
AND Features.id = (FeaturesUserTypes.feature_id))
MySQL
However, if I copy and paste this directly into MySQL I get the results that I expect, all features with only featurs_user_types matching the id are returned.
Actual Query:
SELECT DISTINCT *
FROM features Features
LEFT JOIN features_user_types FeaturesUserTypes
ON (FeaturesUserTypes.user_type_id = 2
AND Features.id = (FeaturesUserTypes.feature_id))
MySQL Output:
----------------------------------------------------------------------------
|ID (feature id)|ID (feature_user_type_id)|feature_id|user_type_id|position|
| 1 | 79 | 1 | 2 | 3 |
| 2 | NULL | NULL | NULL | NULL |
----------------------------------------------------------------------------
CODE
AppController:
My AppController is very generic but built to take it paramters from URLs to generate and execute sql queries. It is a rather large file so instead I went through it with a debugger and recorded any lines that $query was altered and filled in the variables to make it more obvious.
$key = 'FeaturesUserTypes.user_type_id';
$value = 2;
$model = $this->loadModel();
$query = $model->find('all', ['fields' => $this->getFields()]);
$query->contain(['FeaturesUserTypes']);
$query->leftJoinWith('FeaturesUserTypes', function($q) use ($key, $value) {
return $q->where([$key => $value]);
});
$query->distinct();
$results = $query->toArray();
Any idea on what could be happening? I am running CakePHP 3 and PHP 5.6.10. Thanks!
Unlike hasOne and belongsTo associations, which are using joins in the main query unless explicitly configured otherwise, hasMany and belongsToMany associated data is always being retrieved in a separate query.
Note that sql() will always only return the main query, not the possible additional queries used for eager loading associations! If you need to know about the additional queries, check DebugKits SQL log panel. So what this means, is that the query that you're testing manually, is not what the ORM will actually use to retrieve the associated data.
What you're looking for is passing conditions to contain, ie remove the leftJoinWith() (unless you need the associated data in the main query too for further SQL level operations), and attach the condition callback to the FeaturesUserTypes containment instead:
$query->contain([
'FeaturesUserTypes' => function($q) use ($key, $value) {
return $q->where([$key => $value]);
}
]);
And for anyone who reads this, make sure that $key does not hold user input, otherwhise this would be an SQL injection vulnerability!
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Eager Loading Associations Via Contain > Passing Conditions to Contain
Cookbook > Database Access & ORM > Associations - Linking Tables Together > HasMany Associations
It is usually wrong to do filtering in the ON clause:
FROM a
JOIN b ON b.x = 2 AND a.z=b.z
Move the filtering to the WHERE:
FROM a
JOIN b ON a.z=b.z
WHERE b.x = 2
Also, don't use LEFT unless you want the nulls for "missing" rows in the 'right' table.
I don't know your data, but it seems like DISTINCT is not appropriate.

CakePhp3 select() in query more than one tier

Is it possible to get columns of tables which are two or more tables remote by using select()?
$rooms = TableRegistry::get('Rooms')
->find()
->contain(['Levels.Buildings'])
->toArray();
... this works, but returns ervery column of the three tables
$rooms = TableRegistry::get('Rooms')
->find()
->select(['Buildings.name'])
->contain(['Levels.Buildings'])
->toArray();
... this returns nothing, althought the generated select statement seems to be correct
You can only select fields in the primary query if you are handling a one-to-one relationship (i.e. hasOne or belongsTo). This is presumably your issue. In which case you need to specify the fields to include from your has-many relationship in the contain itself:-
$rooms = TableRegistry::get('Rooms')
->find()
->contain([
'Levels' => [
'Buildings' => [
'fields' => 'name'
]
]
])
->toArray();
This is because CakePHP will perform a second separate query to retrieve the has-many associated model data for which you need to specify the query conditions within the contain.

Doctrine/Symfony query builder add select on left join

I have a table of posts related many to one to a table of authors. Both tables are related to a third table (likes) that indicates which users have liked which posts. I'd like to select the authors and likes with the posts, but don't know how to access the joined objects after fetching results. My query builder looks as follows:
$result = $em->createQueryBuilder()
->select('p')
->from('Post', 'p')
->leftJoin('p.author', 'a')
->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10')
->where("p.foo = bar")
->addSelect('a AS post_author')
->addSelect('l AS post_liked')
->getQuery()->getResult();
In the above, the author will always be valid, where the like value may be null if the requesting user (user 10 in the example) has not interacted with the post. The query works fine, but I can't access the data for aliases post_author or post_liked. The resulting data looks like this:
[
[0] => Doctrine PostEntity,
[1] => Doctrine PostEntity,
...
]
I'd like something that looks more like this:
[
[0] => ['post' => Doctrine PostEntity,
'post_author' => Doctrine UserEntity,
'post_liked' => Doctrine LikeEntity],
[1] => ['post' => Doctrine PostEntity,
'post_author' => Doctrine UserEntity,
'post_liked' => Doctrine LikeEntity],
...
]
Were I only trying to load the author, it'd be fine because I could load the author value from the loaded post (Doctrine automatically hydrates the object with selected join data from the author table). For example:
$post = $result[0];
$author = $post->getAuthor(); // Doctrine UserEntity
The issue comes up if I try to load a like for the current user. For example:
$post = $result[0];
$like = $post->getLike(); // INVALID - there's no property "like" on Post
$likes = $post->getLikes(); // valid, but loads the whole collection
$like = $post->post_liked; // INVALID - the SQL alias is not a valid object property
How do I access the data specified in the query?
I ended up using $query->getArrayResults() which populates an array with collections based on the association naming in doctrine configuration. The AS keyword in the query only serves as a hook for the query itself, not the output array/entity hydration.
For a complete solution with examples, see my answer here.

CakePHP: associating two models using different databases?

I have two models, Plant and Emp, that have a Has And Belongs To Many relationship. I've configured them to be associated and the query to get the data for each is correct, but the problem is Plant and Emp are on different databases. Emp is on Database 1, Plant is on Database 2. Because of this they don't query the join table properly; the join table is only on Database 1.
When the Plant model tries to access the join table it's querying Database 2, which does not have this data.
This is the association Emp has for Plant.
var $hasAndBelongsToMany = array(
'Plant' =>
array(
'className' => 'Plant',
'joinTable' => 'emp_plant',
'foreignKey' => 'employee_id',
'associationForeignKey' => 'LocationID',
'unique' => true,
'conditions' => '',
)
);
Update:I tried to set a "finderQuery" attribute to let me query the join table, but I don't know how to give a raw SQL query like that and allow it to dynamically use the id for the instance of the Model instead of a predefined value.
I can set something like
SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = **4**
AND [EmpPlant].[ID] = [Plant].[LocationID])
Which will give me the correct data for one employee, but I don't know how to make this finderQuery a dynamic query. There has to be a way for this to work.
Try
var $useDbConfig = 'alternate';
in your Model Class.
I needed to use a custom finderQuery and use the special {$__cakeID__$} identifier in place of the model ID being matched. This is a fixed version of the sample above, set as the finder query in the relationship entry for the $hasAndBelongsToMany array.
'finderQuery'=>'SELECT * FROM [Plant] AS [Plant] JOIN [DB].[DBO].[empplant] AS
[EmpPlant] ON ([EmpPlant].[employee_id] = {$__cakeID__$}
AND [EmpPlant].[ID] = [Plant].[LocationID])'
This works but if anyone knows how to fix this situation without a custom finder query (what I was trying to avoid by using associations) please post an answer and I will mark that correct instead.

Kohana 3 ORM: How to get data from pivot table? and all other tables for that matter

I am trying to use ORM to access data stored, in three mysql tables 'users', 'items', and a pivot table for the many-many relationship: 'user_item'
I followed the guidance from Kohana 3.0.x ORM: Read additional columns in pivot tables
and tried
$user = ORM::factory('user',1);
$user->items->find_all();
$user_item = ORM::factory('user_item', array('user_id' => $user, 'item_id' => $user->items));
if ($user_item->loaded()) {
foreach ($user_item as $pivot) {
print_r($pivot);
}
}
But I get the SQL error:
"Unknown column 'user_item.id' in
'order clause' [ SELECT user_item.*
FROM user_item WHERE user_id = '1'
AND item_id = '' ORDER BY
user_item.id ASC LIMIT 1 ]"
Which is clearly erroneous because Kohana is trying to order the elements by a column which doesn't exist: user_item.id. This id doesnt exist because the primary keys of this pivot table are the foreign keys of the two other tables, 'users' and 'items'.
Trying to use:
$user_item = ORM::factory('user_item', array('user_id' => $user, 'item_id' => $user->items))
->order_by('item_id', 'ASC');
Makes no difference, as it seems the order_by() or any sql queries are ignored if the second argument of the factory is given.
Another obvious error with that query is that the item_id = '', when it should contain all the elements.
So my question is how can I get access to the data stored in the pivot table, and actually how can I get access to the all items held by a particular user as I even had problems with that?
Thanks
By default, all of Kohana's ORM models expect the table's primary key to be 'id.' You need to set $_primary_key in your model to something else.
$user_item = ORM::factory('user_item', array('user_id' => $user, 'item_id' => $user->items));
I think you need to provide a single item_id value for this to work, not an array of objects.
Also, to find all entries for a single user you should be able to do this:
$user_items = ORM::factory('user_item', array('user_id' => $user));
Does that answer your question?

Categories