Filtering Containable Model Results - cakephp

I am trying with the following line of code to filter all the Posts that have certain (irrelevant) conditions, by the tag "who"
$com= $this->Post->find('all', array('conditions' => $conditions, 'contain' => 'Tag.tag="who"'));
However, instead of the results being only the posts with the tags, I get every single post with empty tag arrays for the ones that don't have the tag "who".
I know my question has come up before, but the solutions posted have not worked with my code.
I have tried adapting the code here to my own:
http://web-development-blog.co.uk/2010/09/14/cakephp-habtm-find-with-conditions-and-containable-behavior/, but I get an SQL error stating "Tag.post_id" not found in on clause.
Please help.
Error message when trying to implement code from selected link:
SELECT `Post`.`id`, `Post`.`title`, `Post`.`body`, `Post`.`created`, `Post`.`modified`, `Tag`.`id`, `Tag`.`tag`, `Tag`.`created`, `Tag`.`modified` FROM `posts` AS `Post` LEFT JOIN `tags` AS `Tag` ON (`Tag`.`post_id` = `Post`.`id`) WHERE `Tag`.`tag` = 'who' 1054: Unknown column 'Tag.post_id' in 'on clause'
Which is caused by using this:
$this->Post->bindModel(array('hasOne' => array('Tag')));
$this->Post->contain(array(
'Tag'
));
$com=$this->Post->find('all', array(
'conditions'=>array('Tag.tag'=>'who')
));

Your error is that you are using bindModel wihtout saying the foreignkey or anything, you SHOULD establish the associations in the model and just use this on-the-fly method when is strictly neccesary.
you need to tell cake which foreignkey of not cake will use the default, in your case it will try Tag.post_id.
Also you should use a has many association or at least i think so. I said this beacause i suppose a post may have multiple tags.
once you do the asociation in the model and delete this line
$this->Post->bindModel(array('hasOne' => array('Tag')));
it should work perfectly
OR you can do the association with the correct parameters on the fly read the cookbook for this, the example that shows how to put the classname is the one you seek, just add the other attributes
EDIT:
Sorry, read something wrong... the has many association doesn't do a join so you have to use contain to specify the condition or add a condition to the association on the fly, here is how your code should look after adding the conditions with contain
$contain = array(
'Tag' => array(
'conditions'=> array('Tag.tag'=>'who')
)
);
$com=$this->Post->find('all', array(
'contain'=>$contain
));
REMEMBER contain conditions override the conditions in the associations!! if you want to have extra conditions you must add them on the fly something like:
$this->Post->hasMany['Tag']['conditions'] += array('Tag.tag'=>'who');
Remember that associations on the fly will work for that instance ONLY.
Also you may do a hasmany alias for different types (remember to define always the classname and foreignkey).
Hope it works this time, if not just comment to see it fix ;)

Related

Associating multiple models in CakePHP

I am currently working on an application in CakePHP to display (but not modify) the contents of a legacy database. I am not able to change the database structure in any way.
There are six tables I am working with and the relationships are outlined in this awful paint diagram:
I have currently defined the models as follows:
http://pastebin.com/4aEaSnQp
(I decided to put it in a PasteBin as it is a bit of a wall of text, I am happy to edit it into the post if preferred).
What I would like to do is use one controller to pass relevant linked data from my controller to the view from essentially all of these tables. However, even if I set recursion to 2 the links only seem to go so far - for example if I try to find data from the 'Ctit' table with code like this:
$this->set('contracts',$this->Ctit->find('all',
array(
'recursion' => 2,
'conditions' => array('Title.TITNO' => $id),
'fields' => array('Contract.CONNO','Title.TITLE','Contract.CONDATE','Territory.DESCRIPTION')
)
));
The resulting query does not include the "Territory" table and so throws back an error.
What am I doing wrong here? Is it possible to retrieve the data in this way or might I have to run multiple finds or write my own queries (something I am hoping to avoid)?
Thanks in advance,
Kez
So... condensing the comments.
Basically it's a Containable problem. To get everything related to a single model (meaning, the 5 other models), a way to do it would be like
$this->Ctit->find('all', array(
'contain' => array(
'Royalty',
'Title',
'Contract' => array('Publisher', 'Territory')
)
You can also add options to that array, like any other query, for example order, fields, conditions, etc.
Example:
$this->Ctit->find('all', array(
'contain' => array(
'Royalty' => array('order' => 'id DESC'),
'Title' => array('fields' => array('id', 'name'),
'Contract' => array('Publisher',
'Territory' => array('conditions' => array('name' => 'terr'))
)
One thing to have in mind is to always use
public $actsAs = array('Containable');
(mind that s in actsAs, some people have had trouble because of that) otherwise your models won't behave containabl-y and you'd think this whole thing doesn't work.
Containable behaviour is useful for this, because you don't have to do a lot of finds, but keep in mind that cake does a lot of queries behind scenes to use containable. If you want this to be just one big query, use joins (found it!)
Also, be aware that this query might get big real quick. You are basically asking for all records with all 5 associations, when you reach a memory limit notice you are going to remember me. I doubt you want to show all data for every record on one view, so reconsider, maybe a simple paginate first and then a single view for each single record (in that case, the find will be a find('first') and that's lot better than find('all').

Removing a belongsTo from a model shows warning Model 'x' is not associated with model 'y'

I am new to CakePHP and I am having to maintain/develop a CakePHP application and struggling with removing a BelongsTo in one of the models.
The code has been copied from another project that used a lot more related tables. In the new project I do not need the relationships because it is creating LEFT JOINS in the queries that I really want to get rid of in order to speed things up. So I went in to the model and removed the $belongsTo property..
When I go to the home page I now get a warniing:-
Warning (512): Model "Product" is not associated with model "ParentProduct"
[CORE/Cake/Model/Behavior/ContainableBehavior.php, line 343]
If I turn off debugging it is not shown but I would like to know why this message is being generated. I am unable to find any information on how I should go about removing the belongsTo relationships.
Any help appreciated.
So I went in to the model and removed the $belongsTo property
Because you're asking for it
This kind of error is displayed when using the containable behavior incorrectly.
E.g. Consider User hasMany Article, and the following code:
$User->find('all', array(
'contain' => array(
'Article'
)
));
This would return all users and their articles (only), irrespective of any other associations that exist.
If an association is requested that isn't defined:
$User->find('all', array(
'contain' => array(
'Article',
'Comment'
)
));
Containable will generate a warning so that you, the developer, can know that there will be no Comments in your results which either indicates a typo or another kind of development error (Ah... it's User<-Article<-Comment).
This is in-keeping with what you describe since the model associations have been modified but (evidently) not the application code.
The message is been generated (most probably) because in your controller (check the ParentProductsController first) is something like
$this->ParentProductsController->Product->find();
That association of models no longer exists because you wanted to get rid of it, so there's no way for ParentProducts to call Product. You need to change calls like that to
ClassRegistry::init('the_name_of_the_model');
(in this case ClassRegistry::init('Product');) or load the model with
$this->loadModel('Product');
Now, this only is necessary when you call models not related to the current controller. If you are calling Product->find from the ProductsController, there will be no error.
Ok, that's the cause of the error.
But now: I don't recommend you deleting the association. There's really no need and your removing a logical association in the code even though it's still there in the database. There are other ways to avoid left joins and useless queries to the database without cutting someone a limb.
If you want to speed things up, read about Containable Behavior, and set all models to $recursive = -1 (recursive doc). That way you won't get LEFT JOINS unless you explicitly say so.

How can I minimize the 'contain' queries in CakePHP?

I have three models, Users, Comments and Pages.
Users has many Comments, and Comments belong to Pages.
All models use the containable behavior, and default to recursive -1.
If I call a find() query on Comments, with the contain request including the Page model's field, this correctly returns the results using a single query, automagically joining the Page table to the user.
If I call a similar query from the User model (containing Comment and Comment.Page), the result is a query to source the Comments, followed by a query per comment to source the relevant Page.
Is there a way to configure the models to maintain the JOIN optimisation? I assumed the belongsTo declaration on the related model (Comments) would follow through to the host model (Users).
UPDATE
I should clarify, my question used a simplified version of my actual case study. Although the minimal solution I require would include this initial Model hasMany Model belongsTo Model structure, I am also looking for the solution at one or more additional belongsTo Models down the chain (which, I though, would automagically use LEFT JOINs, as this would be feasible).
Hmm that's interesting. That's a sort of optimization that should be implemented in the core :)
At any rate, I think you could get the same results (perhaps formatted differently) by building the query a little differently:
$this->User->Comment->find('all', array(
'conditions' => array(
'Comment.user_id' => $userId
),
'contain' => array(
'User',
'Page'
)
));
By searching from the Comment model, it should use two left joins to join the data since they are both 1:1 relationships. Note: The results array may look a little different than from when you search from the User model.
So are you asking if there is an easier way to just contain all your queries? If you want to contain everything within the current controller. You could do the contain in the beforeFilter() callback and it would apply to all your queries within that controller.
I am not quite sure if I understand your question, but I think you have a problem with the many sql-calls for the Comment -> Page linkage? If that is correct, then
try linkable behaviour which reduces sql calls and works almost as contain does
or if its pretty much the same data you want, then create a function in a specific model from where you are happy with hte sql calls (for example the Comment-Model) and call it from the user model by $this->Comment->myFindFct($params);
hope that helps
EDIT: one thing that comes to my mind. You were able to change the join type in the association array to inner, which made cake to single call the associated model as well
I find a good way to do this is to create a custom find method.
As a for instance I'd create a method inside your User model say called _findUserComments(). You'd then do all the joins, contains, etc.. inside this method. Then in your controllers, wherever you need to get all of your user's comments you would call it thusly:
$this->User->find('UserComments', array(
"conditions" => array(
'User.id' => $userId
)
));
I hope this helps.
If model definition like bellow:
Comment model belongs to Page and User.
Page belongs to User and has many Comment.
User has many Page and Comment
code bellow will return one joined query:
$this->loadModel('Comment');
$this->Comment->Behaviors->attach('Containable');
$queryResult = $this->Comment->find('all', array(
'contain' => array(
'User',
'Page'
)
));
The code bellow will return two query. Page and User joined into one query and all comment in another query
$this->loadModel('Page');
$this->Page->Behaviors->attach('Containable');
$queryResult = $this->Page->find('all', array(
'contain' => array(
'User',
'Comment'
)
));
and also bellow code will return three query, one for each model:
$this->loadModel('User');
$this->User->Behaviors->attach('Containable');
$queryResult = $this->User->find('all', array(
'contain' => array(
'Page',
'Comment'
)
));

CakePHP default model fields

I am new to CakePHP, and while I really enjoy the ease of being able to just select a model and all its associated models, I am trying to figure out if there is an easier way to not just have all the fields selected from each model.
For example, instead of just automatically selecting all fields when I grab the model data, and without having to laboriously specify a fields=>array(...) every time, is there some way I can specify which fields are selected by default?
You can also create your own find method in the model:
function findSelected($options = array()) {
$options['fields'] = array('id','name');
return $this->find('all', $options);
}
in controller:
$this->Model->findSelected(array('order' => 'id ASC'));
Even nicer to merge the options array in the findSelected method, then you can even provide additional fields on the fly.
yes, you can check in beforeFind of that model if the 'fields' key is set, if not, you can set it there. But I would say, other than making the debug looks neater, there's almost no performance gain in doing that. And it's one more thing to keep in mind if you have to make changes to the model.
You want to have a look at the Containable behaviour - http://book.cakephp.org/view/1323/Containable
It's very well documented, but to give you a brief overview
$this->Article->find('all', array(
'contain' => array(
'Author.name',
'Category' => array(
'name',
'icon'
)
)
));
would return all your Articles data, along with just the three other fields.
I have Containable added to my app_model definition because I use it on all but the simplest of finds.

CakePHP HABTM: how to find all Photos HABTM multiple Tags?

I have two tables, photos and tags, with a standard photos_tags link table.
My models show Photos HABTM Tags, and I can update both fine with the links updating too.
My question is: say I now want to find all Photos tagged with both "Sunset" and "Ocean" - how do I go about that?
I've tried doing an inner join as per Nate's Bakery post and I can now find all photos tagged with either OR both, but I want just those with both tags (e.g. an "AND" find, if that's the correct phrase). Currently adding more tags gives me more results - I want less results as more tags are added!
Alternatively, if there isn't a nice Cakey way, how would you do it? Multiple searches and then compare the arrays? Seems kinda wasteful though...
TIA.
You can bind condition arrays like this
'conditions' => array('AND' => array('field' => 'value', 'field' => 'value'))

Resources