How can I minimize the 'contain' queries in CakePHP? - 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'
)
));

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').

CakePHP - multiple paginated datasets on same page from same model

I have an admin page that pulls data from my ExpenseClaim model. I want to have 3 different paginated tables, each table displaying data based on a different condition (a status). This is what I have (which doesn't work - nothing is paginated in the view) in my controller:
// Pending
$this->set('claims', $this->paginate('ExpenseClaim', array('ExpenseClaim.claim_status_id' => '2')));
// Get approved
$this->set('approvedClaims', $this->paginate('ExpenseClaim', array('ExpenseClaim.claim_status_id' => '3')));
// Get Declined
$this->set('declinedClaims', $this->paginate('ExpenseClaim', array('ExpenseClaim.claim_status_id' => '4')));
Does anyone know how I can achieve this, I've do a fair amount of searching but only found things relating to different models or hacks using jquery plugins. Surely this can be done in cake alone?
Thanks in advance
Did you wrote the above code inside ExpenseClaimsController? But in case if you in another controller, make sure you have a paginate variable as the following
public $paginate = array(
'ExpenseClaim' => array(
.......
)
);

Filtering Containable Model Results

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 ;)

CakePHP Pagination with HABTM relationship

I have a 'Club' HABTM 'Member' relationship.
From the ClubsController::View($id) view. I wish to show ALL the Members that belong to that ONE Club in a paginating table. I have tried many things but all were not exactly what I was looking for. The most common, seemingly related solution looked like CakePHP multi-HABTM associations with pagination but this would give me ALL the 'Club' that belong to ONE 'Member' from the ClubsController. As apposed to ALL the 'Member' from the ONE 'Club' from the ClubsController. I feel this should be an obvious task.
Im answering because I can't comment on Meibooms answer.
It's wrong, because there shouldn't be a Member.club_id field as its a HABTM relation.
Instead, what you should have is a join table: ClubsMembers.club_id ClubsMembers.member_id
As explained in the answer to the related question you mention, what you need to do is bind a fake hasOne relation to be able to set the conditions you want.
In your particular case:
$this->Club->Member->bindModel(array(
'hasOne' => array('ClubsMember')
), false);
// Set up the paginate options array
$this->paginate['Member'] = array(
'contain' => array(
'ClubsMember',
),
'conditions' => array(
'ClubsMember.club_id' => $club_id,
),
//Group since the HasOne may return multiple rows for each
//(if $club_id is an array of IDs and a Member is in more than one of those).
'group' => 'Member.id',
'order' => 'Member.name',
);
$this->set('members', $this->paginate('Member'));
Ignore the contain part if you don't use ContainableBehavior. (Cake will ignore it anyway.)
It sounds to me like a simple paginate on Member with the conditions of 'Member.club_id' => $id should do the trick for you.
ps. What version of CakePHP are you using? Newer versions are a bit more sophisticated with pagination afaik.

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.

Resources