How do I write a join query across multiple tables in CakePHP? - cakephp

can anyone tell me, how to retrieve joined result from multiple tables in cakePHP ( using cakePHP mvc architecture). For example, I have three tables to join (tbl_topics, tbl_items, tbl_votes. Their relationship is defined as following: a topic can have many items and an item can have many votes. Now I want to retrieve a list of topics with the count of all votes on all items for each topic. The SQL query for this is written below:
SELECT Topic.*, count(Vote.id) voteCount
FROM
tbl_topics AS Topic
LEFT OUTER JOIN tbl_items AS Item
ON (Topic.id = Item.topic_id)
LEFT OUTER JOIN tbl_votes AS Vote
ON (Item.id = Vote.item_id);
My problem is I can do it easily using $this-><Model Name>->query function, but this requires sql code to be written in the controller which I don't want. I'm trying to find out any other way to do this (like find()).

$markers = $this->Marker->find('all', array('joins' => array(
array(
'table' => 'markers_tags',
'alias' => 'MarkersTag',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('MarkersTag.marker_id = Marker.id')
),
array(
'table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array(
'Tag.id = MarkersTag.tag_id',
'Tag.tag' => explode(' ', $this->params['url']['q'])
)
)
)));
as referred to in nate abele's article: link text

I'll be honest here and say that you'll probably be a lot happier if you just create a function in your model, something like getTopicVotes() and calling query() there. Every other solution I can think of will only make it more complicated and therefore uglier.
Edit:
Depending on the size of your data, and assuming you've set up your model relations properly (Topic hasMany Items hasMany Votes), you could do a simple find('all') containing all the items and votes, and then do something like this:
foreach ($this->data as &$topic)
{
$votes = Set::extract('/Topic/Item/Vote', $topic);
$topic['Topic']['vote_count'] = count($votes);
}
Two things are important here:
If you have a lot of data, you should probably forget about this approach, it will be slow as hell.
I've written this from my memory and it might not look like this in real life and/or it may not work at all :-)

You can easily set the "recursive" property on a find() query.
$result = $this->Topic->find('all', array('recursive' => 2));
Alternatively, you can use the Containable behavior in your model. Then you can use:
$this->Topic->contain(array(
'Item',
'Item.Vote',
));
$result = $this->Topic->find('all');
or
$result = $this->Topic->find('all', array(
'contain' => array(
'Item',
'Item.Vote',
),
));

What you need is recursive associations support, which is not possible with stock CakePHP currently.
Although it could be achieved using some bindModel trickery
or an experimental RecursiveAssociationBehavior.
Both of these solutions will either require you to use extra code or rely on a behaviour in your application but if you resist the temptation to write pure SQL code, you'll be rewarded with being able to use Cake`s pagination, auto conditions, model magic etc..

I think this answer is already submitted, but I am posting here for someone who seeks still for this.
The joins can be done with find() method can be like below
$result = $this->ModelName1->find("all",array(
'fields' => array('ModelName1.field_name','Table2.field_names'), // retrieving fileds
'joins' => array( // join array
array(
'table' => 'table_name',
'alias' => 'Table2',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('ModelName1.id = Table2.id') // joins conditions array
),
array(
'table' => 'table_name3',
'alias' => 'Table3',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('Table3.id = Table2.id')
)
)));

You should study HaBTM (Has and Belongs to Many)
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html

Related

Get original associations after using Containable behavior in CakePHP

Background: CakePHP 2.6.3. A pretty stable app. New behavior (MyCustomBehavior) created to output some extra info.
I have a Model MyModel acting as Containable (defined in AppModel) and then MyCustom (defined in MyModel). MyCustomBehavior is written in a way that it needs to work with the Model's associations with other Models in my app.
Problem: Whenever I contain related models in my find() call of MyModel, I cannot get a complete list of MyModel associations because Containable behavior unbinds the models that are not contained. However, if I don't set contain in my find() options or set 'contain' => false everything works as expected.
Sample MyModel->belongsTo
public $belongsTo = array(
'MyAnotherModel' => array(
'className' => 'MyAnotherModel',
'foreignKey' => 'my_another_model_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Creator' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Approver' => array(
'className' => 'User',
'foreignKey' => 'approver_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Status' => array(
'className' => 'Status',
'foreignKey' => 'status_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
);
Sample find()
$this->MyModel->find('all', array(
'fields' => array(...),
'conditions' => array(...),
'contain' => array('Approver', 'Status')
));
Result of MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'Approver' => array(
...
),
'Status' => array(
...
),
);
Expected MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'MyAnotherModel' => array(
...
),
'Creator' => array(
...
),
'Approver' => array(
...
),
'Status' => array(
...
),
);
Obvious solution: One dumb way to solve the problem is to simply set Containable behavior in MyModel instead of AppModel to control the order of loading the behaviors, i.e., public $actsAs = ['MyCustom', 'Containable']. This solution is not the best because there may be other behaviors in other models that depend on Containable, so the order of Containable needs to set in each model in app explicitly and hope that I didn't break the app somewhere.
A similar(related) question was asked on SO here but has no answers.
Need a more robust solution that can address the needs of MyCustomBehavior without having to make changes in rest of the app and looking out for any unexpected behavior.
Attempt 1 (Imperfect, error prone):
One way to recover all the original associations is to call
$MyModel->resetBindings($MyModel->alias);
$belongsToAssoc = $MyModel->belongsTo; // will now have original belongsTo assoc
However, this approach it may fail (SQL error 1066 Not unique table/alias) to work correctly if I had used joins in my find call (using default alias) to explicitly join to an already associated model. This is because Containable will also attempt to join all these tables restored by resetBindings() call resulting in join being performed twice with same alias.
Attempt 2 (perfect#, no known side effects##):
Further digging through the core Containable behavior and docs led me to object $MyModel->__backOriginalAssociation and $MyModel->__backAssociation (weird enough that ContainableBehavior never used $__backContainableAssociation as the variable name suggests) that was created and used by this behavior to perform resetBindings(). So, my final solution was to simply check if Containable is enabled on my modal (redundant in my case because it is attached in AppModel and is never disabled or detached throughout the app) and check if the object is set on the model.
// somewhere in MyCustomBehavior
private function __getOriginalAssociations(Model $Model, $type = 'belongsTo') {
if(isset($Model->__backAssociation['belongsTo']) && !empty($Model->__backAssociation['belongsTo'])) { // do an additional test for $Model->Behaviors->enabled('Containable') if you need
return $Model->__backAssociation[$type];
}
return $Model->$type;
}
public function beforeFind(Model $Model, $query) {
// somewhere in MyCustomBehavior::beforeFind()
...
$belongsToAssoc = $this->__getOriginalAssociations($MyModel, 'belongsTo'); // will now have original belongsTo assoc
...
return $query;
}
$__backAssociation holds model associations temporarily to allow for dynamic (un)binding. This solution can definitely be further improved by merging results of $Model->belongsTo and $Model->__backAssociation['belongsTo'] (or hasMany, hasOne, hasAndBelongsToMany) to include any models that were bound on the fly. I don't need it, so I will skip the code for merging.
# Perfect for my own use case and my app setup.
## No side effects were found in my testing that is limited by my level of expertise/skill.
Disclaimer: My work in this post is licensed under WTF Public License(WTFPL). So, do what the f**k you want with the material. Additionally, I claim no responsibility for any financial, physical or mental loss of any kind whatsoever due to the use of above material. Use at your own risk and do your own f**king research before attempting a copy/paste. Don't forget to take a look at cc by-sa 3.0 because SO says "user contributions licensed under cc by-sa 3.0 with attribution required." (check the footer on this page. I know you never noticed it before today! :p)

find() based on conditions in multiple HABTM relations

I'm working with CakePHP 2.1
Let's state I have the following models and relations:
Posts belongsTo Edition
Posts HABTM Editors
Posts HABTM Tags
I'm reasoning from the PostsController and I would like to find all Posts belonging to a certain Edition, and that Editor.id=55 is related and Tag.id=33 is related.
I have found many examples where finding Posts based on a condition over a HABTM relation is done by 'reversing' the find direction and reason from the Editor.
$this->Editor->find('all',
array(
'conditions'=>array('Editor.id'=>55),
'contain'=>array('Post')
)
);
Unfortunately this doesn't work here, because there are multiple HABTM relations that I would like to put a condition on.
Also using contain does not work since it only cuts off a branch (editors) but does not help filtering it's parent (posts).
How do I solve this?
Do I maybe need to resort to ad hoc joins? And if so, could someone explain me roughly what approach to take?
Edit:
Some pseudo code in order to illustrate what I want to achieve:
$this->Post->find('all',
array('conditions'=>array(
'Edition.id'=>4,
// start pseudo code
'Post.Editor' => 'has at least one related editor with id=55',
'Post.Tag' => 'has at least one related tag with id=33',
)
)
);
kind regards,
Bart
Edit Solution:
Following #RichardAtHome I created the solution below. Not (yet) employing multiple join conditions, but that appeared not to be necessary:
// Paginate over Cards that belong to current Subscription (belongsTo) AND the User is related to as an Editor (HABTM)
$this->paginate = array(
'fields' => array('id','title','workingtitle','attachment_count','subscription_id'),
'joins' => array(
// create Editor-join
array(
'table' => 'cards_users',
'alias' => 'Editor',
'type' => 'left',
'conditions' => array(
'Editor.card_id = Card.id'
)
),
array(
'table' => 'users',
'alias' => 'User',
'type' => 'left',
'conditions'=> array(
'User.id = Editor.user_id'
)
)
),
'conditions' => array(
'OR' => array(
// is Card in current subscription? (straightforward condition)
array('Card.subscription_id',$subscription_id),
// is current User assigned as Editor? (condition works with join above)
array('User.id' => $user['id'])
)
),
'limit' => 10
);
For this type of query, I usually find it easiest to construct the joins myself as cake won't join, it will perform multiple queries instead (select matching rows from table a, then fetch matching rows from table b).
Your query needs to look something like this:
$this->Post->find('all',
array(
'conditions'=>array('Post.edition_id'=>4),
'joins'=>array(
array(
'type'=>'inner',
'table'=>'posts_editors',
'alias'=>'PostsEditor',
'conditions'=>array(
'PostsEditor.post_id = Post.id',
'PostsEditor.editor_id'=>55 // your criteia for editor id=55
)
),
array(
'type'=>'inner',
'table'='posts_tags',
'alias'=>'PostsTag',
'conditions'=>array(
'PostsTag.post_id = Post.id',
'PostsTag.tag_id'=>33 // your criteria for tag id = 33
)
)
)
)
);
Check the SQL that's output to see what this query is actually doing behind the scenes.
Try:
$this->Post->find('all',
array('conditions'=>array(
'Edition.id'=>4,
'Editor.id' => '55',
'Tag.id' => '33',
)
)
);

cakePHP virtualField within condition of another model

I have 2 models, User and Entity. I need to on my entities page, have pagination, and a simple search function. The search function will filter the entities, but the data it filters is a virtualField within the User model.
The Entity model:
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
};
The virtual field in the User model:
public $virtualFields = array("searchFor" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email)");
And the condition within the entity controller:
$conditions["User.searchFor LIKE"] = "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%";
$this->paginate = array(
'conditions' => $conditions,
'order' => array('Re.created' => 'DESC'),
'limit' => 20
);
From what I can read this is not possible because I cannot use virtualFields within a query of an associative model, and it says I must use it in "run time", but I really do not understand how to do that, or how to do that in this instance. Any ideas or a way I can get this to work?
You could try attaching the virtualField to the Entity model like so
$this->Entity->virtualFields['searchFor'] = $this->Entity->User->virtualFields['searchFor'];
But you have to make sure that a join is done and not 2 queries.
I believe its discussed in the book.
Edit: Book page
For this purpose I needed to do a search on a concatinated string from an associated model. Normally this can be done using Virtualfields in cake, but cake does not support using a virtualField from an associated model in the search.
Seeing that the 2 models were already linked with the belongsTo, I merely changed the condition to:
"conditions" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email) LIKE" => "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%"
Probably not the most elegant solution, but it works.

cakePHP - how do i get the items count for a related model?

The models are: stores and product, and they are associated by:
var $belongsTo = array(
'Store' => array(
'className' => 'Store',
'foreignKey' => 'store_id'
))
var $hasMany = array(
'Product' => array(
'className' => 'Product',
'foreignKey' => 'store_id'
))
I want to get a list of all stores and the count of products they have. How should I modify the call: $this->Store->find('all', array(<..some conditions..>)) to return that type of data?
One method is to use Cake's built-in counterCache option in your association. This is probably the most performant option, though it does require adding a field to your table.
In your stores table, add an INT field called product_count
In your Product model add the counterCache option to your association:
var $belongsTo = array(
'Store' => array(
'className' => 'Store',
'foreignKey' => 'store_id',
'counterCache' => true
));
Whenever you add or delete Product records, it will automatically update the product_count field of the associated Store record, so that there is no need to alter your find operations.
Note that if you choose this route, you will need to manually update the product_count field for the initial value to be correct, as it only updates after add/delete operations.
I believe something like the following would work, however I cannot test it from here. The COUNT() contents might need tweaking to work with how Cake constructs its queries.
$this->Store->virtualFields = array('product_count' => 'COUNT(Product.id)');
$this->Store->find('all', array(
'fields' => array('Store.id', 'Store.product_count'),
'group' => array('Store.id'),
));
After CakePHP 2.0 you can also add conditions to your count and multiple counters per model.
Add any integer field to your Store then use this in your Product model:
var $belongsTo = array(
'Store' => array(
'className' => 'Store',
'foreignKey' => 'store_id',
'counterCache' => array(
'anyfield', array('Product.id >' => 0),
'stock' => array('Product.status' => 'stock')
)
)
);
Check out find('count').
However, this may not scale well if your looking to pair this data with store data (see the answer by therealtomrose).
I've had trouble using find('count') and grouping data. Unfortunately this is one of those edge cases where frameworks writing queries for you fails.
A simple find all will fetch the data you need:
$Stores = $this->Store->find('all');
The count of products at one of the stores can be returned with the following code where '0' can be replaced with the number of the store's array index:
count($Stores[0]['Products']); //returns an integer
If you want the count of products at each store, you could consider looping over all stores thus:
foreach ($Stores as $Store) {
echo count($Store['Products']);
}
Assumptions I am making:
You want the count of products at each store, not product count overall.
You're not too concerned with performance. If you have more than 50,000 records or so, you might want to consider pairing this back, but it will be considerably more complicated.

Is it possible to use bindModel to bind 3 different nested tables in CakePHP

I have a segment which can have many comments and each comment can have many tags. I can bind the comments to the segments using code like the below which is a function in the segment model class.
function prepareForGettingSegmentsWithComments() {
$this->bindModel(
array('hasMany' => array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'segmentID'
)
)
)
);
}
However how can I bind in the Tags as well?
Yes, in this situation, I bind the Tags to the Comments with a belongsTo. Then filter the results with some query conditions.
Let me see if I can find an example snippet somewhere,
if(isset($this->params['named']['category'])){
$this->Link->bindModel(
array('belongsTo' => array(
'CategoriesLink' => array(
'className' => 'CategoriesLink',
'foreignKey' => 'id',
)
)),
array('belongsTo' => array(
'Category' => array(
'className' => 'Category',
'foreignKey' => 'categories_link_id',
)
))
);
$data = $this->paginate('Link', array('CategoriesLink.category_id'=>$this->params['named']['category']));
} else {
$data = $this->paginate('Link', array('Link.status_id'=>'1'));
}
$this->set('links', $data);
This is how I did it when I was trying to paginate my Link model by a related field. This was with Cake1.2 though, but I think the principle is the same.
I would also recommend installing DebugKit, http://www.ohloh.net/p/cakephp-debugkit , then tinker with the links and conditions until you get a query which works for you.
Sorry, not very technical ;) I'm sure someone can give you a more accurate answer.
PS, Having just reread the question, do you not have these Models linked already? Surely hooking them up in the Models through CakePHP relationships, you'd not need to bind the models and could just use Containable or unbindModel()

Resources