This question is similar to Find conditions like 'NOT EXISTS' except that it's working with a hasMany relationship.
The tables are:
questions
id bigint
...
answers
id bigint
question_id bigint
...
The relationship is questions hasMany answers.
The query is to look for question ids that have no answers.
SQL might look like
select id from questions where not exists
(select * from answers where answers.question_id = questions.id)
The quick way is to just run a query() with the statement, but I'd like to know if there is a CakePHP way.
I'd like to avoid a NOT IN scenario, since that may result in two hits to the database; One to get all question ids for questions that do have answers, and the second to get all question ids for questions that don't have answers.
The other way might be to put the whole where clause in the conditions array as a single entry. I'm just not sure if that is best practice.
Without having to alter the database, I ended up using the following query:
$questions = $this->Questions->find('all', array(
'fields' => array('id'),
'conditions' => array('not exists '.
'(select id from answers '.
'where answers.question_id = '.
'Question.id)'
)
));
The best way to do this is to have a count field in your posts table. Cake has a built in feature called counter cache.
class Post extends AppModel
{
var $name = "Post";
var $hasMany = array('Answer'=>array('counterCache'=>true));
}
You will need to add answer_count to your posts table. This column will be updated AUTOMATICALLY on adding and deleting of associated records.
Then your query is a simple find:
$this->Post->find('all', array('conditions' => array('answer_count' => 0)));
Documentation found here:
http://book.cakephp.org/view/1033/counterCache-Cache-your-count
Related
I think this is a common pattern, but I can't find the elegant CakePHP way of doing it. The idea is to remove the values from a list which have already been chosen. To use an example from the Cake book:
table Students (id int, name varchar)
table Courses (id int, name varchar)
table CoursesMemberships (id int, student_id int, course_id int, days_attended int, grade varchar)
All I want to do is create a query which returns the courses that a given student has not yet signed up for, most probably to populate a select dropdown.
If I weren't using Cake, I'd do something like
select * from Courses where id not in
(select course_id from CoursesMemberships where student_id = $student_id)
Or maybe an equivalent NOT EXISTS clause, or outer join trickery, but you get the idea.
I'm stumped how to do this elegantly in Cake. It seems to me that this would be a common need, but I've researched for awhile, as well as tried some query ideas, to no avail.
If you want to use a subquery, simply pass a query object as the condition value, like
$subquery = $Courses->CoursesMemberships
->find()
->select(['CoursesMemberships.course_id'])
->where(['CoursesMemberships.student_id' => $student_id]);
$query = $Courses
->find()
->where([
'Courses.id NOT IN' => $subquery
]);
As an alternative there's also Query::notMatching() (as of CakePHP 3.1), which can be used to select records whichs associated records do not match specific conditions:
$query = $Courses
->find()
->notMatching('CoursesMemberships', function (\Cake\ORM\Query $query) use ($student_id) {
return $query
->where(['CoursesMemberships.student_id' => $student_id]);
});
See also
Cookbook > Database Access & ORM > Query Builder > Subqueries
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Using notMatching
Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
In CakePHP 3 you can create NOT IN query like this.
$query = $Courses->find()
->where(['id NOT IN' => $ids]);
And in CakePHP 3 you can create IN query like this.
$query = $Courses->find()
->where(['id IN' => $ids]);
You can read about it in CakePHP 3 Cookbook - Creating IN Clauses.
Found IMO the most elegant answer... use the notMatching() option:
$data = $this->Courses->find("list")
->notMatching("Students",
function($q) use ($student_id) {
return $q->where(["Students.id"=>$student_id]);
}
);
This assumes that Students HasMany Courses and Courses HasMany Students of course.
I think this is the most elegant answer since it doesn't depend on knowing any SQL, and represents the logic of what I'm trying to achieve using Cake's semantics only.
In CakePHP 3 ORM has changed and I can't find the proper way to select needed data from the database.
In CakePHP 2, I use contain('User.name','User.id'), but In CakePHP 3 this code doesn't work.
So how can I select only id and name from User?
The code:
$query = $data->find()->contain(['Users'])->execute()->fetchAll('assoc');
// I want only user.id and user.name.
$articles = $this->Model->find()
->select(['fields_you_want_from_this_Model'])
->contain(['Assoc_Model' => function($q) {
return $q
->select(['fields_you_want_from_the_associated_model']);
}]);
U must take a look about this page: http://book.cakephp.org/3.0/en/orm/query-builder.html#passing-conditions-to-contain
In certain case you must use autoFields method.
Be carefull with contain when u select few fields in the callable, u always have to select the foreign key also:
When you limit the fields that are fetched from an association, you must ensure that the foreign key columns are selected. Failing to select foreign key fields will cause associated data to not be present in the final result.
Sorry if this is obvious - I'm trying to do a find() on a join table and failing, whats the correct syntax to do this?
Basically I have expenseCode table with a HABTM releationship with the expense table.
In my expenseCode model I have:
public function beforeDelete($cascade = false) {
$count = $this->Expense->find("count", array(
'conditions' => array('expense_code_id' => $this->id)
));
if ($count == 0) {
return true;
} else {
//$this->Session->setFlash('Record cannot be deleted as it has ' . $count . 'number of expenses attached to it');
return false;
}
}
If I uncomment the setFlash() I get the error.
Where am I going wrong? Thanks in advance.
Unfortunately it's not possible to perform such a query on the HABTM join table from one of the parent models without some extra work. I'm assuming that Expense is the parent model and ExpensesCode the join model?
A common way is to modelise the HABTM join table. Say you have an expenses and codes table which are joined by expenses_codes:
$this->Expense->bindModel(array('hasOne' => array('ExpensesCode')));
$this->Expense->find('all', array(
'fields' => array('Expense.*'),
'conditions' => array('ExpensesCode.expense_code_id' => $this->id)
));
However, Cake also auto-iniatlises a model for the join table when a HABTM association is defined (see the manual, and the "with" key in the list of possible keys).
So this syntax would let you directly query the join table:
$this->Expense->ExpensesCode->find('all', array(
'conditions' => array('ExpensesCode.expense_code_id' => $this->id)
));
The query above will net you with an array containing only the results from the join table, as it doesn't perform a join like the first procedure. So you would have to perform a second find() on the Expense model to find the expenses related to the expense_code_id from ExpensesCode.
EDIT:
It's a framework convention that HABTM join tables should be underscored and alphabetically ordered. So if the HABTM join table is called codes_expenses, it's auto-modelised as CodesExpense.
You can also work in a simpler way if you create another model with two hasMany relations instead of one with HABTM.
HasAndBelongsToMany between two models is in reality shorthand for three models associated through both a hasMany and a belongsTo association.
I found it is one of the simplest ways to save and retrieve data.
For more information:
http://book.cakephp.org/2.0/en/models/saving-your-data.html#what-to-do-when-habtm-becomes-complicated
I have problem with updating (better updating not recreating) extra field in HABTM join table. I searched google and other sources, but struggled for 4 days now.
I have models:
class Tutorial extends AppModel {
var $hasAndBelongsToMany = array(
'TutorialCategory' => array(
'with' => 'TutorialCategoriesTutorial',
'order' => 'TutorialCategoriesTutorial.order_by ASC',
'unique' => true,
);
}
class TutorialCategory extends AppModel {
var $hasAndBelongsToMany = array(
'Tutorial' => array(
'with' => 'TutorialCategoriesTutorial',
'unique' => true,
);
}
join table tutorial_categories_tutorial have id, tutorial_id, tutorial_category_id, order_by fields.
I am trying to update order_by field like:
$order = 1;
foreach($tutorials as $i => $tutorial) {
$this->data[$i]['Tutorial']['id'] = $tutorial['Tutorial']['id];
$this->data[$i]['TutorialCategory']['id'] = $tutorial['TutorialCategory']['id];
$this->data[$i]['TutorialCategoriesTutorial']['order_by'] = $order;
++$order;
}
$this->Tutorial->bindModel(array('hasMany' => array('TutorialCategoriesTutorial')));
$saved = $this->Tutorial->saveAll($this->data);
This is deleting and crating new records in join table, but not setting order_by at all. I want to update record and set now order_by value. I tried hasMany through but no luck.
Please help and/or give advice and explanation.
Thank you!
As you have added extra data (order field) to the HABTM join model, you have actually exceeded the capabilities of a simple HABTM relationship with CakePHP. What you actually need to setup is a hasMany Through relationship.
In your case you'll basically make a "membership" model with Tutorial ID, catergory id and as much data as you want to assign to it. You will then define the relatioships as Membership belongsTo Tutorial & Category. The book probably has a better example than what I've just explained!
The main reason for this is that each "membership" record is treated as a normal record with no HABTM behaviour attached to it, so you can edit, delete and add records individually and easily.
For an e-commerce app that I'm building I am using CakePHP.
I have created the db and afterwards baked my app with cake bake.
My models are all linked up properly as follows:
Product hasMany CategoryProduct,ImagesProduct
Category hasMany CategoryProduct
CategoryProduct belongsTo Product,Category
Image hasMany ImagesProduct
ImagesProduct belongsTo Product,Image
I have tried in various ways to obtain a paginated view of products dependent of the category_id with no succes.
I have managed to obtain the array I wanted with
$this->CategoryProducts->findByCategoryId($id), but I can't use the built-in paginator from cakePHP with the resultant array.
Could somebody tell me how to properly formulate the $options['joins'] array to pass on to the paginate function in order to get the same result, but in a paginated way?
The SQL for what I want to do would be something like this:
SELECT p . * , i.filename
FROM products p
LEFT JOIN (
category_products cp, categories c, images_products ip, images i
) ON ( cp.product_id = p.id
AND c.id = cp.category_id
AND c.id =2
AND ip.product_id = p.id
AND ip.image_id = i.id )
This is a question that perplexed me for quite sometime. You shouldn't have to associate either of your join models (CategoryProduct, ImagesProduct) directly to your models if you're using a HABTM association with CakePHP. cake bake may not have picked it up the HABTM association correctly if you didn't have the table names quite right. The join tables should be categories_products and images_products, which would make th join models CategoriesProduct and ImagesProduct.
Either way though, the following should get you going on filtering by categories:
//in products_controller.php:
//Fake a HasOne association, setting the reset parameter to false since pagination
//requires two queries to complete (one for the count, one for the data)
$this->Product->bindModel(array(
'hasOne' => array(
'CategoriesProduct
)
), false);
//Group by products since the HasOne will return multiple rows for each product
$options = array(
'group' => 'Product.id',
'conditions' => array(
'CategoriesProduct.category_id' => $categories
)
);
//To paginate the result:
$this->paginate = $options;
$products = $this->paginate();
In this example $categories can be either a single category_id or an array containing category_ids (i.e. array('1', '2', '3')). The result will be an 'OR' of categories. If you're looking to filter by an 'AND' type condition, check http://edblogs.cal.msu.edu/devteam/2011/02/08/cakephp-habtm-searches/.
Hope this helps.