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.
Related
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?
Task
I'm trying to return a set of data based on a condition in the related model.
The problem
Currently the closest I can get is using Containable to return all matching model data, but only returning child data if it matches the contain condition. This isn't ideal as my data still contains the primary model data, rather than it being removed.
I am using a HABTM relationship, between, for example, Product and Category, and I want to find all products in a specific category.
Inital idea
The basic method would be using containable.
$this->Product->find('all', array(
'contain' => array(
'Category' => array(
'conditions' => array(
'Category.id' => $categoryId
)
)
)
));
Although this will return all products, and just remove the Category dimension if it doesn't match the contain condition.
Closest so far
$this->Product->find('all', array(
'contain' => false,
'joins' => array(
array(
'table' => 'categories_products',
'alias' => 'CategoriesProduct',
'type' => 'LEFT',
'conditions' => array(
'CategoriesProduct.product_id' => 'Product.id'
)
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'LEFT',
'conditions' => array(
'Category.id' => 'CategoriesProduct.category_id'
)
)
),
'conditions' => array(
'Product.status_id' => 1,
'Category.id' => $categoryId
),
));
Which generates the following query,
SELECT `Product`.`id`, `Product`.`name`, `Product`.`intro`, `Product`.`content`, `Product`.`price`, `Product`.`image`, `Product`.`image_dir`, `Product`.`icon`, `Product`.`icon_dir`, `Product`.`created`, `Product`.`modified`, `Product`.`status_id`
FROM `skyapps`.`products` AS `Product`
LEFT JOIN `skyapps`.`categories_products` AS `CategoriesProduct` ON (`CategoriesProduct`.`product_id` = 'Product.id')
LEFT JOIN `skyapps`.`categories` AS `Category` ON (`Category`.`id` = 'CategoriesProduct.category_id')
WHERE `Product`.`status_id` = 1
AND `Category`.`id` = 12
This query is correct, except that the join conditions are being quoted ' instead of `, which breaks the query.
Manual query
SELECT *
FROM products
JOIN categories_products ON categories_products.product_id = products.id
JOIN categories ON categories.id = categories_products.category_id
WHERE categories.id = 12
The problem lay in the way I was defining my join conditions. It's not an associative array but rather a string.
'conditions' => array(
'CategoriesProduct.product_id' => 'Product.id'
)
Changes to
'conditions' => array(
'CategoriesProduct.product_id = Product.id'
)
Im new to cakePHP and the whole table relations concept is very confusing!
I have 2 tables, competencies and competenceRatings. competencies stores a list of names with ids.
competencies
------------
id
name
And users can select various competencies from this table and rate them and their ratings are stored into competenceRatings table.
competenceRatings
-----------------
id
competence_id
user_id
rating
I want to be able to get the names of competencies for which a user have NOT made any ratings into competenceRatings table. i.e., I need list of names from competencies table for which there are no entries in comptenceRatings table(for given user_id).
I tried competencies->hasMany->competenceRatings, competenceRatings->belongsTo->competencies relations.
$competencies = $this->Competence->CompetenceRating->find('all',array('CompetenceRating.user_id' => $userId,'CompetenceRating.competence_id !=' => 'Competence.id'));
But no use!
Does this result require any other relations? Or can i just join tables using joins condition in find query?
Thanks.
EDIT
This method worked:
$options['joins'] = array(
array(
'table' => 'competence_ratings',
'alias' => 'CompetenceRating',
'type' => 'LEFT OUTER',
'conditions' => array(
'Competence.id = CompetenceRating.competence_id',
'CompetenceRating.user_id' => $userId
)
)
);
$options['conditions'] = array( 'CompetenceRating.competence_id'=> null );
Try this
Joining tables
$data = $this->Competence->find('all', array('joins' => array(
array(
'table' => 'competenceRatings',
'alias' => 'CompetenceRating',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('CompetenceRating.competencie_id = Competence.id')
),
array(
'table' => 'competencies',
'alias' => 'Competence',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array(
'Competence.id = MarkersTag.tag_id',
'Competence.user_id' => $user_id
)
)
)));
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.
I've completely confused myself now.
I have three tables: applicants, applicants_qualifications, and qualifications.
In the index view for applicants, I have a form with a dropdown of qualifications. The results should be a list of applicants with that qualification.
So I need the table of applicants on the index view to be based on a join, right?
If I add this to my applicants_controller:
$options['joins'] = array(
array(
'table' => 'applicants_qualifications',
'alias' => 'q',
'type' => 'left outer', // so that I get applicants with no qualifications too
'conditions' => array('Applicant.id = q.applicant_id',)
)
);
$this->Applicant->find('all', $options);
I get an additional sql statement at the bottom of the page, with the left outer join but the sql without the join is there too.
I think this line:
$this->set('applicants', $this->paginate());
calls the sql statement without the join.
Looks like I need to combine the join $options with the paginate call. Is that right?
If I use the search form, I get: Unknown column 'Qualifications.qualification_id' in 'where clause'
So the page is obviously not yet 'using' my sql with the join.
Sorry - I'm still a noob. Any help appreciated...
In order to set conditions, joins, etc for your model's pagination, you must do it as follows:
function admin_index() {
$this->Applicant->recursive = 0;
$this->paginate = array(
'Applicant' => array(
// 'conditions' => array('Applicant.approved' => true),
'joins' => array(
array(
'table' => 'applicants_qualifications',
'alias' => 'ApplicationsQualification',
'type' => 'left outer',
'conditions' => array('Applicant.id = ApplicationsQualification.applicant_id')
)
)
// 'order' => array('Applicant.joined DESC')
)
);
$this->set('applicants', $this->paginate());
}
I've commented out some sample keys that you can include later on - just to give you an idea of how it works.
Hope that helps!
You can use inner join instead of left outer join.For eg.
in cource controller
$cond = array(
array(
'table' => 'colleges',
'alias' => 'Colleges',
'type' => 'inner',
'conditions'=> array('Colleges.college_id = Courses.college_id')
)
) ;
$courses = $this->Courses->find('all',
array('joins' =>$cond,'conditions' => array("Courses.status ='1' AND Colleges.status='1' "),
'order'=>array('course_name'),
'fields' => array('course_id','course_name'),'group'=>'Courses.course_id'
)
);