Pagination with Containable conditions work with hasOne, but not with hasMany - cakephp

For example, I have this relationship:
UserContact hasMany Contact
Contact hasOne Info
Contact hasMany Response
And I need to paginate Contact, so I use Containable:
$this->paginate = array(
'limit'=>50,
'page'=>$page,
'conditions' =>array('Contact.id'=>$id),
'contain'=>array(
'Response',
'Info'
)
);
I want to add search by Info.name, and by Response.description. It works perfect for Info.name, but it throws an error if I try using Response.description, saying that the column doesn't exist.
Additionally, I tried changing the relationship to Contact hasOne Response, and then it filters correctly, but it only returns the first response and this is not the correct relationship.
So, for example, if I have a search key $filter I'd like to only return those Contacts that have a matching Info.name or at least one matching Response.description.

If you look at how CakePHP constructs SQL queries you'll see that it generates contained "single" relationships (hasOne and belongsTo) as join clauses in the main query, and then it adds separate queries for contained "multiple" relationships.
This makes filtering by a single relationship a breeze, as the related model's table is already joined in the main query.
In order to filter by a multiple relationship you'll have to create a subquery:
// in contacts_controller.php:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$dbo = $this->Contact->getDataSource();
$subQuery = $dbo->buildStatement(array(
'fields' => array('Response.id'),
'table' => $dbo->fullTableName($this->Contact->Response),
'alias' => 'Response',
'conditions' => $conditionsSubQuery
), $this->Contact->Response);
$subQuery = ' EXISTS (' . $subQuery . ') ';
$records = $this->paginate(array(
'Contact.id' => $id,
$dbo->expression($subQuery)
));
But you should only generate the subquery if you need to filter by a Response field, otherwise you'll filter out contacts that have no responses.
PS. This code is too big and ugly to appear in the controller. For my projects I refactored it into app_model.php, so that each model can generate its own subqueries:
function makeSubQuery($wrap, $options) {
if (!is_array($options))
return trigger_error('$options is expected to be an array, instead it is:'.print_r($options, true), E_USER_WARNING);
if (!is_string($wrap) || strstr($wrap, '%s') === FALSE)
return trigger_error('$wrap is expected to be a string with a placeholder (%s) for the subquery. instead it is:'.print_r($wrap, true), E_USER_WARNING);
$ds = $this->getDataSource();
$subQuery_opts = array_merge(array(
'fields' => array($this->alias.'.'.$this->primaryKey),
'table' => $ds->fullTableName($this),
'alias' => $this->alias,
'conditions' => array(),
'order' => null,
'limit' => null,
'index' => null,
'group' => null
), $options);
$subQuery_stm = $ds->buildStatement($subQuery_opts, $this);
$subQuery = sprintf($wrap, $subQuery_stm);
$subQuery_expr = $ds->expression($subQuery);
return $subQuery_expr;
}
Then the code in your controller becomes:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$records = $this->paginate(array(
'Contact.id' => $id,
$this->Contact->Response->makeSubQuery('EXISTS (%s)', array('conditions' => $conditionsSubQuery))
));

I can not try it now, but should work if you paginate the Response model instead of the Contact model.

Related

How to do query search on child models while fetching the parent model?

I have the following setup:
Job hasMany Quotation
Quotation belongsTo Job
Quotation hasMany QuotationsInBatch
Batch hasMany QuotationsInBatch
I want to do $this->find on the following conditions:
Job.name OR
Quotation.quotation_number OR
Quotation.site_text
retrieving the following results:
the various Job records and the Quotations that belongTo the Job and the various Batches that are associated with the Quotation
Meaning to say if I type abc and
a Job.name matches abc, I will display that Job and its associated Quotations and associated Batch OR
a Quotation.quotation_number matches abc, I will display the parent Job and all its associated Quotations and associated Batch OR
a Quotation.site_text matches abc, I will display the parent Job and all its associated Quotations and associated Batch
Please advise.
UPDATE:
I got this to work. But essentially, I use find two times and joins
First I do this:
$options = array(
'conditions' => $conditions,
'order' => array(
'Job.id' => 'DESC',
),
'fields' => $fields
);
$joins = array(
array('table' => 'quotations',
'alias' => 'Quotation',
'type' => 'LEFT',
'conditions' => array(
'Quotation.job_id = Job.id',
)
)
);
$options['joins'] = $joins;
$results = $this->find('all', $options);
Because my conditions largely are concerned with Job and Quotation, I do a search with joins between these two models and only return Job.id
After which I check if there are Jobs that match the criteria, and then perform another find. This time, with the other associated Models involved.
if ($results) {
$ids = Hash::extract($results, '{n}.Job.id');
} else {
$ids = array('Job.id' => -1000); // put a fake id
}
$options['conditions'] = array('Job.id' => $ids);
$joins[] = array('table' => 'quotations_in_batches',
'alias' => 'QuotationInBatch',
'type' => 'LEFT',
'conditions' => array(
'Quotation.id = QuotationInBatch.quotation_id',
)
);
$joins[] = array('table' => 'batches',
'alias' => 'Batch',
'type' => 'LEFT',
'conditions' => array(
'Batch.id = QuotationInBatch.batch_id',
)
);
Then perform a find using these options.
Is there a better way than mine?

Containable with Condition

I am using containable with CakePHP. My tried code is ...
public function detail($slug = "") {
$this->Poet->contain('User.id', 'User.full_name', 'Song.id', 'Song.name', 'Song.name_hindi', 'Song.slug');
$result = $this->Poet->findBySlug($slug);
if (!$result) {
throw new NotFoundException(__('Invalid Poet - ' . $slug));
}
pr($result);
die();
$this->Poet->id = $result['Poet']['id'];
$this->set('result', $result);
}
Like this. Now I have Song.status as my association with Song table. I want to fetch only those records that has status = 1. Is it possible? Can I select only active records with my piece of code.
Use a normal find
While the magic findBy* methods are handy from time to time, it's a good idea to only use them for trivial queries - your query is nolonger trivial. Instead use a normal find call e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
)
),
'conditions' => array(
'slug' => $slug,
'Song.status' => 1 // <-
)
));
Does a Poet hasMany songs?
You don't mention your associations in the question, which is rather fundamental to providing an accurate answer, however it seems likely that a poet has many songs. With that in mind the first example will generate an sql error, as there will be no join between Poet and Song.
Containable does permit filtering associated data e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
'Song.status = 1' // <-
)
),
'conditions' => array(
'slug' => $slug
)
));
This will return the poet (whether they have relevant songs or not), and only the songs with a status of "1". You can achieve exactly the same thing by defining the condition in the association definition (either directly in the model or by using bindModel).

AND / OR conditions cakephp FIND for has many association

I have a model Post that has many association with the model Comment.
Post has a primary key post_id which is Comment s foreign key.
Both of these have a visible column.
I have a working query on Post.visible options, and I need to add the AND to find all Posts that have one of Post.visible values.
For these posts I need all Comments that have a Comment.visible value = 1.
My code:
$conditions = array(
"OR" => array(
"Post.visible" => array(
1,
2,
3,
4
),
),
"AND" => array (
"Comment.visible" => 1
)
);
$result = $this->Post->find('all', array(
'order' => 'Post.created DESC',
'conditions' => $conditions
));
The result without the AND is OK (but I get also the Comments with visible = 0).
When I put the condition "Comment.visible" => 1 in the has manyassociation, it works well (but I can not do this, because I need to get the Comment with visibility 0 elsewhere).
With the and it shows this Error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Comment.visible' in 'where clause'
When I dump the SQL, the comments table is not even matched in the SELECT clause (nor in the LEFT JOIN).
You can limit another model's results using CakePHP's Containable Behavior with something like this (this should work, but feel free to tweak per your needs):
//Post model
public $recursive = -1;
public $actsAs = array('Containable');
public function getPosts() {
$posts = $this->find('all',
array(
'conditions' => array(
'Post.visible' => 1
),
'contain' => array(
'Comment' => array(
'conditions' => array('Comment.visible' => 1)
)
)
)
);
return $posts;
}
Or, you can set up your association to only ever pull comments that are visible (even WITH this way, I still recommend using 'contain' like above - you just wouldn't need to specify the condition each time):
//Post model
public $hasMany = array(
'Comment' => array(
'conditions' => array('Comment.visible' => 1)
)
);

Cakephp complex find condition with count on child table

I have two tables topics and posts. Relation: topics hasmany posts. In both table, there is a status field (Y, N) to moderate the content. In my page, I want to list all not moderated topics for which at least one post status is N or topic status itself is N. Is it possible to do with find function in cakephp2.0. Im using Containable behavior.
I need to apply pagination too.
This is one solution:
Search on the Post model for (Posts with N status) OR (Posts which belong to Topics with N status) and store the topic_id
Now search on the Topic model for topics with ID on the list
Something like this:
# TopicsController.php
$ids = $this->Topic->Post->find('list', array(
'fields' => array('Post.topic_id')
'conditions' => array(
'OR' => array(
'Post.status' => 'N',
'Topic.status' => 'N',
)
)
));
$this->paginate = array(
'conditions' => array('Topic.id' => (array)$ids),
'order' => array('Topic.created' => 'DESC')
);
$topics = $this->paginate('Topic');
Since you're searching on the Posts model, CakePHP will join the parent Topic data and you can filter by both statuses.
If i understand correctly you can use:
$conditions => array ('OR' => array ('Topic.status' => 'N', 'Post.status' => 'N'));
Well I haven't tested it but following should work
$this->recursive = -1; //necessary to use joins
$options['joins'] = array(
'table' => 'posts',
'alias' => 'Post',
'type' => 'left',
'conditions' => array('Topic.id = Post.topic_id', 'Post.status = N') //updated code
);
$options['group'] = array('Topic.id HAVING count('Topic.id') >= 1 OR Topic.status = N');
$this->Topic->find('all', $options);

conditions in find method

I have 3 models,
Mentor, MentorAttrib, Attrib where MentorAttrib is a join table. it lists Mentor.id -> Attrib.id
This is my find
$cond = array("Mentor.is_listed"=>1);
$contain_cond = array();
$contain = array(
'MentorAttrib' => array(
'fields' => array('MentorAttrib.id' ,'MentorAttrib.attrib_id'),
'Attrib'
)
);
if(! empty($this->request->data))
{
debug($this->request->data);
//skills
if(! empty($this->request->data['bookingSkills']))
{
$cond = array('MentorAttrib.attrib_id' => $this->request->data['bookingSkills']);
}
}
$this->request->data = $this->Mentor->find('all', array(
'conditions' => $cond,
'fields' => array('Mentor.id','Mentor.first_name','Mentor.last_name','Mentor.img'),
'contain' => $contain
));
I want to filter the result by the skills.
[bookingSkills] => Array
(
[0] => 2
[1] => 4
[2] => 10
)
The error im getting is
Column not found: 1054 Unknown column 'MentorAttrib.attrib_id' in 'where clause'
This is the data set
http://pastebin.com/85uBFEfF
You need a join
By default, CakePHP will only create joins for hasOne and belongsTo associations - any other type of association generates another query. As such you cannot filter results based on a condition from a hasMany or hasAndBelongsToMany association by just injecting conditions and using contain.
Forcing a join
On option to achieve the query you need is to use the joins key. As shown in the docs You can also add a join, easily, like so:
$options['joins'] = array(
array('table' => 'mentor_attribs',
'alias' => 'MentorAttrib',
'type' => 'LEFT',
'conditions' => array(
'Mentor.id = MentorAttrib.mentor_id',
)
)
);
$data = $this->Mentor->find('all', $options);
This allows the flexibility to generate any kind of query.

Resources