JOIN in CakePHP - A better understanding - cakephp

I'd like to understand joins in CakePHP (v 2.4.5) a bit better by solving the following example:
Post hasMany Comment
Post.id == Comment.post_id
Comment.published can be 1 or 0
I need to find all Posts that have at least one published Comment
I want to write the query from the Post model. In order not to break pagination and so I can add order/conditions based on Post
I do not want to filter out results afterwards in PHP (in order not to break pagination)
You might suggest to approach this issue from the Comment model like here:
https://stackoverflow.com/a/3890461/155638
But this is about understanding joins better, so I'd like to set a requirement to write the query from the Post model.
I have roughly the following idea, hoping that the RIGHT join would exclude all non conforming Posts:
$this->Post->find('all', array(
'joins' => array(
array(
'table' => 'comments',
'alias' => 'CommentsJoined',
'type' => 'RIGHT',
'conditions' => array(
'Post.id = CommentsJoined.post_id',
'CommentsJoined.published = true'
)
)
),
'contain' => array(
'Comment' => array(
'conditions' => array(
'Comment.published' => 1
)
)
)
);
But it did not work for me yet.
Currently my query returns 19 times the same Post, instead of 19 unique Posts.
How to go from here? Is the approach the right one?
Kind regards!
Bart

It seems I was on the right track. The final step was to remove the duplicate Posts.
This is done by adding 'group' => 'Post.id' as an attribute to the query.
Like this:
$this->Post->find('all', array(
'joins' => array(
array(
'table' => 'comments',
'alias' => 'CommentsJoined',
'type' => 'RIGHT',
'conditions' => array(
'Post.id = CommentsJoined.post_id',
'CommentsJoined.published = true'
)
)
),
'group' => 'Post.id',
'contain' => array(
'Comment' => array(
'conditions' => array(
'Comment.published' => 1
)
)
)
);

Related

CakePHP only get results that have a related model

In my CakePHP 2 project, I have Projects that have many Articles, an Article can belong to many projects (a many-to-many relation).
Now I would like to find all Projects that have an Article.
My current code for getting the Projects is as follows
$projects = $this->Project->find('list', array(
'fields' => array('Project.slug', 'Project.name')
));
I tried adding contain to the query, without results
$projects = $this->Project->find('list', array(
'contain' => array('PressArticles' => array()),
'fields' => array('Project.slug', 'Project.name')
));
How can I modify this so I receive all projects that have an article?
Containing won't help, non-1:1 relations will be retrieved in a separate query, so that won't have any effect on your main query.
You could for example manually create INNER joins with the association's join and target table, that would automatically filter out all projects that have no associated articles, something along the lines of this (I've more or less guessed the names, it's just a quick and dirty example):
$projects = $this->Project->find('list', array(
'fields' => array('Project.slug', 'Project.name'),
'joins' => array(
array(
'table' => 'press_articles_projects',
'alias' => 'PressArticleProject',
'type' => 'INNER',
'conditions' => array(
'PressArticleProject.project_id = Project.id',
),
),
array(
'table' => 'press_articles',
'alias' => 'PressArticle',
'type' => 'INNER',
'conditions' => array(
'PressArticle.id = PressArticleProject.press_article_id',
),
)
),
'group' => 'Project.id'
));
Or if you are using a counter cache, then filtering by the counter would be an option too:
$projects = $this->Project->find('list', array(
'fields' => array('Project.slug', 'Project.name'),
'conditions' => array(
'Project.press_article_count >' => 0
)
));
See also
Cookbook > Models > Associations: Linking Models Together > Joining tables
Cookbook > Models > Associations: Linking Models Together > counterCache - Cache your count()
In your Project model, you can do
public $hasMany = array(
'PressArticle' => array(
'className' => 'PressArticle',
'foreignKey' => 'project_id'
)
);
For more info, check here

CakePHP conditions for deep associations

I have some deep associations using containable and need to filter back the results. For the sake of this question, let's say we are selling cars and want to narrow the results down by features.
Car hasmany make hasmany model HABTM features
$options = array(
'order' => array('Car.price'),
'contain' => array(
'make',
'model' => array(
'order' => 'Model.name ASC'
),
'features'
)
);
$cars = $this->Car->find('all', $options);
How would I go about excluding all cars that don't have power windows (Features.name != power_windows).
Containable is only suitable for you to specify what models you wanted to include when fetching data, but not limiting the parent model from fetching data at all. One obvious symptom is that sometimes your parent data may have some null contained data.
So to achieve it, I think we should use joins here so you can specify condition:
$options = array(
'order' => array('Car.price'),
'contain' => array(
'make',
'model' => array(
'order' => 'Model.name ASC'
),
'features'
),
'joins' => array(
array(
'table' => 'features',
'alias' => 'Feature',
'type' => 'LEFT',
'conditions' => array(
'Car.id = Feature.car_id'
)
)
),
'conditions' => array(
'Features.name !=' => 'power_windows',
)
);
But one drawback of this is that you might have duplicated Car due to joining. That's a separate issue ;)

Add tables to custom query in cakephp

Im trying to replicate the following query in cakephp:
SELECT *
FROM uploads, proposals
WHERE proposals.id = uploads.proposal_id AND proposals.tender_id = 10
Im using the find method in the Upload model with the following conditions:
$conditions = array(
'Proposal.id' => $id,
'AND' => array(
'Upload.proposal_id' => 'Proposal.id'
)
);
return($this->find('list', array('conditions' => $conditions)));
but im getting this query instead
SELECT `Upload`.`id`, `Upload`.`title`
FROM `kumalabs_lic`.`uploads` AS `Upload`
WHERE `Proposal`.`id` = 10 AND `Upload`.`proposal_id` = 'Proposal.id'
as you can see, the proposals table is missing, can somebody explain me how can i make this query?
Thanks :)
I would recommend you use the linkable behaviour for this. It is much easier than the default way of doing joins in CakePHP. It works with the latest version of CakePHP, as well as 1.3.
CakePHP Linkable Behavior
You would then modify your find to look like this:
return($this->find('list', array(
'link' => array('Proposal'),
'conditions' => array(
'Proposal.id' => $id,
),
'fields' => array(
'Upload.*',
'Proposal.*',
),
)));
CakePHP will automatically join on your primary / foreign key, so no need to have the
'Upload.proposal_id' => 'Proposal.id'
condition.
Though you don't need that condition, I also want to point out that you are doing your AND wrong. This is how you do AND and OR in CakePHP
'conditions' => array(
'and' => array(
'field1' => 'value1', // Both of these conditions must be true
'field2' => 'value2'
),
'or' => array(
'field1' => 'value1', // One of these conditions must be true
'field2' => 'value2'
),
),
If the model have association, CakePHP automatically join the table by 'contain' keyword. Try code bellow:
public function getProposalsFromTender($id){
$data = $this->find('all', array(
'conditions' => array('Proposal.id' => $id),
'fields' => array('Upload.*', 'Proposal.*'),
'contain' => array('Proposal')
));
return($data);
}
Note:
CakePHP use explicit join instead of implicit join like ...from proposals, uploads...
I'm not familiar with that JOIN syntax but I believe it equals this:
SELECT *
FROM uploads
INNER JOIN proposals ON proposals.id = uploads.proposal_id
WHERE proposals.tender_id = 10
... so you need something similar to this:
// Untested
$conditions = array(
'Proposal.id' => $id,
'joins' => array(
array(
'alias' => 'Proposal',
'table' => 'proposals',
'type' => 'INNER',
'conditions' => 'Proposal.id = Upload.proposal_id',
),
),
);
Of course, this is a direct translation of your JOIN. If your models are properly related, it should all happen automatically.

cakephp unable to sort on afterfind virtual fields with paginate

I have a query that I am running through paginate. This query contains a model ("PaymentException") that has an afterfind method that tacks on a copy of the last "ExceptionWorkflowLog", and calls it "LastWorkflowLog".
The query being passed to paginate:
$this->paginate = array(
'fields' => array(
'PaymentException.*', 'Procedure.id', 'Procedure.cpt',
'Procedure.expected_amount', 'Procedure.allowed_amount', 'Procedure.difference_amount',
'Claim.id', 'Claim.number', 'Payer.abbr'
),
'limit' => 50,
'joins' => array(
array(
'table' => 'procedures',
'alias' => 'Procedure',
'conditions' => array('Procedure.id = PaymentException.procedure_id')
),
array(
'table' => 'claims',
'alias' => 'Claim',
'conditions' => array('Claim.id = Procedure.claim_id')
),
array(
'table' => 'payers',
'alias' => 'Payer',
'conditions' => array('Payer.id = Procedure.payer_id')
),
array(
'table' => 'groups',
'alias' => 'Groups',
'conditions' => array('Groups.id = Claim.group_id')
)
),
'conditions' => $conditions,
'contain' => array('ExceptionWorkflowLog')
);
The resulting array (from the query that combines both "PaymentException", "ExceptionWorkflowLog", and "LastWorkflowLog") looks like below:
0 =>
'PaymentException' => array(fields and values),
'ExceptionWorkflowLog' => array(of ExceptionWorkflowLogs),
'LastWorkflowLog' => array(fields and values of the last indexed ExceptionWorkflowLog)
1 => ...
ExceptionWorkflowLog is mapped to PaymentException by PaymentException.id. It's a many to one relationship (thus the array of results under the ExceptionWorkflowLog).
I would like to use paginate to sort on the "updated" field on either the last indexed ExceptionWorkflowLog or the LastWorkflowLog.
Is there a way to do this with paginate? Currently, if I set the table heading to point to "LastWorkflowLog.updated", the query returns false because the query doesn't know what "LastWorkflowLog" is.
Since this has a couple hundred views, I figured I'd come back and post what I did. CakePHP's handling of joins is absolutely terrible. I rewrote the query to not use joins, but use contains. That seems to have solved it. I feel dirty.

Only retrieve records with at least one association

This is pretty straightforward, but I guess I've never bumped into it before. I have a Page model that hasMany Comment. I'd like to pull all of the pages that have at least 1 comment, but eliminate pages with none. As I look at it, I realize that I'm not sure how to do that. I guess I could use ad hoc joins, but I'd rather use Containable, if that's possible. I've tried testing not null in the Comment conditions and one or 2 other things that were unlikely to work, but it seems like this should be possible.
What I get now, of course, is all pages and some of those page records have an empty Comment member. Be nice to skip passing around all of the extra cruft if I can do so.
My find call:
$pages = $this->Folder->Page->find(
'all',
array(
'contain' => array(
'Comment' => array(
'order' => array( 'Comment.modified DESC' ),
),
'Folder' => array(
'fields' => array( 'Folder.id' ),
),
),
'conditions' => array(
'Folder.group_id' => $id,
),
)
);
Thanks.
You have several approaches available other than ad-hoc joins:
Denormalize your dataset and add a has_comment flag to your pages table. Add 'Page.has_comment' => 1 to your conditions.
Run your query against Comment, with DISTINCT page_id.
$comments = $this->Folder->Page->Comment->find('all', array(
'conditions' => array('DISTINCT Comment.page_id'
'contain' => array(
'Page' => array(
'Folder'
)
)
);
First grab a distinct set of page ids from the comments table.
$page_ids = $this->Folder->Page->Comment->find('list', array(
'fields' => array('id', 'page_id'),
'conditions' => array('DISTINCT Comment.page_id'),
);
$pages = $this->Folder->Page->find('all', array(
'conditions' => array('Page.id' => $page_ids),
'contain' => array(
...
)
);

Resources