The Code
Say I have two models, named Product and Image, which are linked by Product hasMany Image and Image belongsTo Product.
Now, say I want to fetch all products with the first image each. I would use this code:
$this->Products->find('all')
->contain([
'Images' => function($q) {
return $q
->order('created ASC')
->limit(1);
}
]);
Looks about right, right? Except now only one of the products contains an image, although actually each product contains at least one image (if queried without the limit).
The resulting Queries
The problem seems to be with the limit, since this produces the following two queries (for example):
SELECT
Products.id AS `Products__id`,
FROM
products Products
and
SELECT
Images.id AS `Images__id`,
Images.product_id AS `Images__product_id`,
Images.created AS `Images__created`
FROM
images Images
WHERE
Images.product_id in (1,2,3,4,5)
ORDER BY
created ASC
LIMIT 1
Looking at the second query, it is quite obvious how this will always result in only one image.
The Problem
However, I would have expected the Cake ORM to limit the images to 1 per product when I called limit(1).
My question: Is this an error in how I use the ORM? If so, how should I limit the number of images to one per image?
The cleanest way you can do this is by creating another association:
$this->hasOne('FirstImage', [
'className' => 'Images',
'foreignKey' => 'image_id',
'strategy' => 'select',
'sort' => ['FirstImage.created' => 'DESC'],
'conditions' => function ($e, $query) {
$query->limit(1);
return [];
}
])
Check it ,this one is my code
$this->Orders->hasOne('Collections', [
'className' => 'Collections',
'foreignKey' => 'order_id',
'strategy' => 'select',
'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
$query->order(['Collections.id' => 'ASC']);
return [];
}
]);
$Lists = $this->Orders->find('all')->where($condition)->contain(['Collections'])->order(['Orders.due_date DESC']);
$this->set(compact('Lists'));
If the reason you are limiting it to one image is that you want to have a defaulted image. You might consider adding a default field in the image table and doing a Alias like this:
var $hasOne = array(
'CoverImage' => array(
'className' => 'Image',
'conditions' => array('CoverImage.default' => true),
),
It looks like you are using Cake v3 so you can just add the equivalent association as the above is a 2.x sample.
In the Model definition we would do the following:
public $hasMany = [
'ModelName' => [
'limit' => 1,
'order' => 'ModelName.field DESC'
],
];
as described in the docs: https://book.cakephp.org/2/en/models/associations-linking-models-together.html#hasmany
Related
I’m struggling with a self-referencing belongsToMany association. To be clear I have a Models table and each model can have multiple accessories, which are also models. So I have a linking table, Accessories, with a model_id (the “parent” model) and an accessory_id (the “child” model).
I finally found how to declare this in the ModelsTable :
$this->belongsToMany('AccessoryModels', [
'className' => 'Models',
'through' => 'Accessories',
'foreignKey' => 'model_id',
'targetForeignKey' => 'accessory_id'
]);
$this->belongsToMany('ParentAccessoryModels', [
'className' => 'Models',
'through' => 'Accessories',
'foreignKey' => 'accessory_id',
'targetForeignKey' => 'model_id'
]);
I also got it working to retrieve these data in the Models view.
But I now have some issues for the addAccessory (and deleteAccessory) method in the Models controller and views.
Here is it in the controller :
public function addAccessory($id = null)
{
$model = $this->Models->get($id, [
'contain' => []
]);
if ($this->getRequest()->is(['patch', 'post', 'put'])) {
$accessory = $this->getRequest()->getData();
if ($this->Models->link($model, [$accessory])) {
return $this->redirect(['action' => 'view', $model->id]);
}
}
$models = $this->Models
->find('list', ['groupField' => 'brand', 'valueField' => 'reference'])
->order(['brand' => 'ASC', 'reference' => 'ASC']);
$this->set(compact('model', 'models'));
}
The view is only a select dropdown with the list of all available models (I'm using a plugin, AlaxosForm, but it takes the original CakePHP control() function behaviour) :
echo $this->AlaxosForm->label('accessory_id', __('Accessory'), ['class' => 'col-sm-2 control-label']);
echo $this->AlaxosForm->control('accessory_id', ['options' => $models, 'empty' => true, 'label' => false, 'class' => 'form-control']);
echo $this->AlaxosForm->button(___('Submit'), ['class' => 'btn btn-default']);
The problem is that the addAccessory() function won't work when getting the submitted data from the form. I see the problem, as when posting the inserted values, only an array with one accessory_id is given (for example ['accessory_id' => 1] and the link() doesn't know what to do with it. So I think it’s an issue about data formatting but don’t see how to get it correctly.
Saving links (like any other ORM saving methods) requires to pass entities, anything else will either be ignored, or trigger errors.
So you have to use the accessory_id to obtain an entity first, something like:
$accessoryId = $this->getRequest()->getData('accessory_id');
$accessory = $this->Models->AccessoryModels->get($accessoryId);
Furthermore you need to use the model/table that corresponds to the target entities (second argument) that you want to link (to the first argument), ie you'd have to use AccessoryModels, like:
$this->Models->AccessoryModels->link($model, [$accessory])
See also
Coobook > Database Access & ORM > Saving Data > Associate Many To Many Records
I want to display a drop-down list of the list of student name with its value, student_user_id. But the problem is student name is stored in a different model which is hard to align together in find('list'), a find function of Cakephp.
I have some code of the model project that already joins tables. When I code the getStudentList, it does not know the User.user_display_user field.
Model
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => false,
'conditions' => array('User.user_id = Project.student_user_id')
)
);
function getStudentList()
{
$entry = $this->find('list',array('fields' => array('Project.student_user_id','User.user_display_name')));
return $entry;
}
So: How I can handle this issue?
You should use containable behavior and tell CakePHP that in this case you want to join those two tables:
$entry = $this->find('list', array(
'fields' => array('Project.student_user_id', 'User.user_display_name'),
'contain' => array('User')
));
By default joins should not be made until specifically requested - for performance reasons.
See http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
Note:
$recursive = 0 would also work but is discouraged to use.
*EDIT/UPDATE*
I found a solution but don't really know why this has to be. If anyone could answer why this is that would be great.
I found that the MediaFloorplan would show up in my data when I added "floorplan_id" to my field list for MoveInHome being that MoveInHome hasMany Floorplan. Strange because "id" is found within the fields for the contained "Floorplan"
*END EDIT/UPDATE*
I'm working on a project for a home builder and am having a problem getting some data via containable.
App model has this:
public $actsAs = array("Containable");
I'm making a call from FloorplansController to MoveInHome model
$this->loadModel("MoveInHome");
$moveInReadyData = $this->MoveInHome->getByCommunity($communityid,4);
MoveInHome belongsTo Floorplan
Floorplan hasMany MediaFloorplan
Here's the function in the model:
public function getByCommunity($id,$limit=4) {
$params = array(
"fields" => array(
"address",
"city",
"state",
),
"conditions" => array(
"MoveInHome.community_id" => $id,
"MoveInHome.active" => 1,
"MoveInHome.sold" => 0
),
"limit" => $limit,
"order" => "RAND()",
"contain" => array(
"MediaMoveInHome" => array(
"order" => array(
"featured" => "DESC"
),
"fields" => array(
"media_move_in_home_file_path",
),
"limit" => 1,
),
"Floorplan" => array(
"fields" => array(
"id",
"name",
"style",
"bedrooms",
"bathrooms",
"sf_bsmt",
"sf_1flr",
"sf_2flr"
),
"MediaFloorplan" => array(
"conditions" => array(
"MediaFloorplan.type" => "main",
),
"fields" => array(
"id",
"media_floorplan_file_path"
)
)
)
)
);
return $this->find("all", $params);
}
All of the data comes back fine except for that MediaFloorplan doesn't come back at all. Note that I've made a similar contain request in other areas of the site. Would the problem stem from making the request from the FloorplansController to MoveInReady and trying to contain Floorplan, etc?
Thanks for the help.
As you found yourself, and DavidYell clarified in his comment on the question, you have to include the foreign keys in the fields option.
From the actual CakePHP documentation: here
When using ‘fields’ and ‘contain’ options - be careful to include all foreign keys that your query directly or indirectly requires. Please also note that because Containable must to be attached to all models used in containment, you may consider attaching it to your AppModel.
Bounty:
+500 rep bounty to a GOOD solution. I've seriously banged my head against this wall for 2 weeks now, and am ready for help.
Tables/Models (simplified to show associations)
nodes
id
name
node_type_id
node_associations
id
node_id
other_node_id
node_types
id
name
General Idea:
A user can create node types (example "TV Stations", "TV Shows", and "Actors"...anything). If I knew ahead of time what the node types were and the associations between each, I'd just make models for them - but I want this to be very open-ended so the user can create any node-types they want. Then, each node (of a specific node-type) can relate to any other node of any other node-type.
Description and what I've tried:
Every node should be able to be related to any/every other node.
My assumption is that to do that, I must have an association table - so I made one called "node_associations" which has node_id and other_node_id.
Then I set up my association (using hasMany through I believe):
(below is my best recollection of my set-up... it might be slightly off)
//Node model
public $hasMany = array(
'Node' => array(
'className' => 'NodeAssociation',
'foreignKey' => 'node_id'
),
'OtherNode' => array(
'className' => 'NodeAssociation',
'foreignKey' => 'other_node_id'
)
);
//NodeAssociation model
public $belongsTo = array(
'Node' => array(
'className' => 'Node',
'foreignKey' => 'node_id'
),
'OtherNode' => array(
'className' => 'Node',
'foreignKey' => 'other_node_id'
)
);
At first, I thought I had it - that this made sense. But then I started trying to retrieve the data, and have been banging my head against the wall for the past two weeks.
Example Problem(s):
Lets say I have a the following nodes:
NBC
ER
George Clooney
Anthony Edwards
Tonight Show: Leno
Jay Leno
Fox
Family Guy
How can I set up my data structure to be able to pull the all TV Stations, and contain their TV Shows, which contain their Actors (as example)? This would be SIMPLE with normal model setup:
$this->TvStation->find('all', array(
'contain' => array(
'TvShow' => array(
'Actor'
)
)
));
And then, maybe I want to retrieve all male Actors and contain the TV Show which contain the TV Station. Or TV Shows that start at 9pm, and contain it's actor(s) and it's station...etc etc.
But - with HABTM or HasMany Through self (and more importantly, and unknown data set), I wouldn't know which field (node_id or other_node_id) the model is, and overall just can't wrap my head around how I'd get the content.
The Idea
Let's try to solve this with convention, node_id will be the model who's alias comes alphabetically first and other_node_id will be the one that comes second.
For each contained model, we create a HABTM association on-the-fly to Node class, creating an alias for each association (see bindNodes and bindNode method).
Each table we query we add an extra condition on node_type_id to only return results for that type of node. The id of NodeType is selected via getNodeTypeId() and should be cached.
For filtering results using condition in deeply related associations, you would need to manually add extra join, creating a join for each jointable with a unique alias and then joining each node type itself with an alias to be able to apply the conditions (ex. selecting all TvChannels that have Actor x). Create a helper method for this in Node class.
Notes
I used foreignKey for node_id and associationForeignKey for other_node_id for my demo.
Node (incomplete)
<?php
/**
* #property Model NodeType
*/
class Node extends AppModel {
public $useTable = 'nodes';
public $belongsTo = [
'NodeType',
];
public function findNodes($type = 'first', $query = []) {
$node = ClassRegistry::init(['class' => 'Node', 'alias' => $query['node']]);
return $node->find($type, $query);
}
// TODO: cache this
public function nodeTypeId($name = null) {
if ($name === null) {
$name = $this->alias;
}
return $this->NodeType->field('id', ['name' => $name]);
}
public function find($type = 'first', $query = []) {
$query = array_merge_recursive($query, ['conditions' => ["{$this->alias}.node_type_id" => $this->nodeTypeId()]]);
if (!empty($query['contain'])) {
$query['contain'] = $this->bindNodes($query['contain']);
}
return parent::find($type, $query);
}
// could be done better
public function bindNodes($contain) {
$parsed = [];
foreach($contain as $assoc => $deeperAssoc) {
if (is_numeric($assoc)) {
$assoc = $deeperAssoc;
$deeperAssoc = [];
}
if (in_array($assoc, ['conditions', 'order', 'offset', 'limit', 'fields'])) {
continue;
}
$parsed[$assoc] = array_merge_recursive($deeperAssoc, [
'conditions' => [
"{$assoc}.node_type_id" => $this->nodeTypeId($assoc),
],
]);
$this->bindNode($assoc);
if (!empty($deeperAssoc)) {
$parsed[$assoc] = array_merge($parsed[$assoc], $this->{$assoc}->bindNodes($deeperAssoc));
foreach($parsed[$assoc] as $k => $v) {
if (is_numeric($k)) {
unset($parsed[$assoc][$k]);
}
}
}
}
return $parsed;
}
public function bindNode($alias) {
$models = [$this->alias, $alias];
sort($models);
$this->bindModel(array(
'hasAndBelongsToMany' => array(
$alias => array(
'className' => 'Node',
'foreignKey' => ($models[0] === $this->alias) ? 'foreignKey' : 'associationForeignKey',
'associationForeignKey' => ($models[0] === $alias) ? 'foreignKey' : 'associationForeignKey',
'joinTable' => 'node_associations',
)
)
), false);
}
}
Example
$results = $this->Node->findNodes('all', [
'node' => 'TvStation', // the top-level node to fetch
'contain' => [ // all child associated nodes to fetch
'TvShow' => [
'Actor',
]
],
]);
I think you have incorrect relations between your models. I guess it will be enough with:
// Node Model
public $hasAdBelongsToMany = array(
'AssociatedNode' => array(
'className' => 'Node',
'foreignKey' => 'node_id'
'associationForeignKey' => 'associated_node_id',
'joinTable' => 'nodes_nodes'
)
);
// Tables
nodes
id
name
node_type_id
nodes_nodes
id
node_id
associated_node_id
node_types
id
name
Then you can try using ContainableBehavior to fetch your data. For Example, to find all TVShows belonging to a TVStation:
$options = array(
'contain' => array(
'AssociatedNode' => array(
'conditions' => array(
'AssociatedNode.node_type_id' => $id_of_tvshows_type
)
)
),
conditions => array(
'node_type_id' => $id_of_tvstations_type
)
);
$nodes = $this->Node->find('all', $options);
EDIT :
You can even have second level conditions (see last example on this section, look at the 'Tag' model conditions). Try this:
$options = array(
'contain' => array(
'AssociatedNode' => array(
'conditions' => array(
'AssociatedNode.node_type_id' => $id_of_tvshows_type
),
'AssociatedNode' => array(
'conditions' => array( 'AssociatedNode.type_id' => $id_of_actors_type)
)
)
),
conditions => array(
'node_type_id' => $id_of_tvstations_type
)
);
$nodes = $this->Node->find('all', $options);
I think unfortunately part of the problem is that you want your solution to contain user data in the code. Since all your nodes types are user data, you want to avoid trying to use those as the classes methods in your application, as there could be infinite of them. Instead I would try and create methods that model the data operations you want to have.
One omission I see in the provided data model is a way to record the relationships between types. In your example you mention a relationship between TvStation -> TvShows -> Actor etc. But where are these data relationships defined/stored? With all of your node types being user defined data, I think you'll want to/need to record store those relationships somewhere. It seems like node_types needs some additional meta data about what the valid or desired child types for a given type are. Having this recorded somewhere might make your situation a bit simpler when creating queries. It might help to think of all the questions or queries you're going to ask the database. If you cannot answer all those questions with data that is in the database, then you are probably missing some tables. Model associations are just a proxy for data relations that already exist in your tables. If there are gaps there are probably gaps in your data model.
I don't think this is the answer you're looking for but hopefully it helps you find the right one.
Why don't you create a method in the node model?
Something like:
<?php
// first argument is a nested array filled with integers
(corresponding to node_type_id)
//second one id of a node
//third one corresponds to the data you want(empty at beginning in most case)
public function custom_find($conditions,$id,&$array){
//there may several type of nodes wanted: for instances actors and director of a serie, so we loop
foreach($conditions as $key_condition=>$condition){
//test to know if we have reached the 'bottom' of the nested array: if yes it will be an integer '2', if no it will be an array like '2'=>array(...)
if(is_array($condition))){
//this is the case where there is deeper levels remaining
//a find request: we ask for the node defined by its id,
//and the child nodes constrained by their type: ex: all actors of "Breaking Bad"
$this->id=$id;
$result=$this->find('all',array(
'contain' => array(
'OtherNode' => array(
'conditions'=>array('node_type_id'=>$key_condition)
)
)
)
);
//we add to $array the nodes found. Ex: we add all the actors of the serie, with type_id as key
$array[$key_condition]=$result['OtherNode'];
//Then on each node we just defined we call the function recursively. Note it's $condition not $conditions
foreach($array[$key_condition] as &$value){
$this->custom_find($condition,$value['Node']['id'],$value);
}
}else{
//if we simply add data
$this->id=$id;
$result=$this->find('all',array(
'contain' => array(
'OtherNode' => array(
'conditions'=>array('node_type_id'=>$value)
)
)
)
);
$array[$condition]=$result['OtherNode'];
}
}
}
That code is almost certainly wrong, it's just to give you an idea of what I mean.
Edit:
What it does:
it's a recursive function that takes a nested array of conditions and the id of a node and gives back nested array of nodes.
For instance: $conditions=array('2','4'=>array('5','6'=>array('4')))
How it works:
For a single node it gives back all the child nodes corresponding to the condition in the array: then it does the same for the children with the conditions one level deeper, until there is no more levels left.
In my CakePHP app I have models for Matches and Teams. Each Match has a home_team_id and an away_team_id, both of which reference a different Team.
In my team.php file, I am able to form the relationship for a Team's home matches:
var $hasMany = array(
'HomeMatch' => array('className' => 'Match', 'foreignKey' => 'home_team_id'),
'AwayMatch' => array('className' => 'Match', 'foreignKey' => 'away_team_id')
);
My problem is that I cannot automatically retrieve a Team's home and away Matches in a single array. That is, the retrieved Matches are returned in separate HomeMatch and AwayMatch arrays, which causes sorting difficulties.
I have tried the following:
var $hasMany = array(
'Match' => array('foreignKey' => array('home_team_id', 'away_team_id'))
);
...with no luck.
Any ideas on how to combine these two foreign keys into a single relationship?
Thanks, Ben
A custom finderQuery should do the trick:
public $hasMany = array(
'Match' => array(
'className' => 'Match',
'foreignKey' => false,
'finderQuery' => 'SELECT *
FROM `matches` as `Match`
WHERE `Match`.`home_team_id` = {$__cakeID__$}
OR `Match`.`away_team_id` = {$__cakeID__$}'
)
);
I was having a similar issue and instead of creating a finderQuery I used the conditions operator and it worked great!
public $hasMany = array(
'Match' => array(
'className' => 'Match',
'foreignKey' => false,
'conditions' => array(
'OR' => array(
array('Match.home_team_id' => '{$__cakeID__$}'),
array('Match.away_team_id' => '{$__cakeID__$}')
)
),
)
);
They are returned in seperate array's because the sort of represent different models (in this particular case the model is the same).
You should probably build a helper method to go over the retrieved data (in the model object or in a separate helper class) and "flatten" it. then you'd be able to sort it.
Ken.