Cakephp 3 NOT IN query - cakephp

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.

Related

Change join contain() condition in Cakephp

I try to use contain to make an full join of 2 Tables but i don't know how to change the condition:
$select= $this->Friends->find('all')
->order(['Friends.id' => 'ASC'])
->contain([
'Animals'
])
->where(['animal1_id'=> $animalsid,
'confirmed'=>'1'
]);
$this->set(compact('select'));
And so look the SQL:
SELECT
*
FROM
friends Friends
INNER JOIN animals Animals ON Animals.id = (Friends.animal2_id)
WHERE
(
animal1_id = 4
AND confirmed = 1
)
ORDER BY
Friends.id ASC
The problem is that this Friends.animal2_id i want to change with Friends.animal1_id but i don't know how? Maybe there another methods? I tried to use join but i get just one table not both.
I am considering that you have belongsTo relationship between the two model friends and animals.
So you just have to change the foreignKey name in the association, so update the association in your Friends model like
$this->belongsTo('Animals', [
'foreignKey' => 'animal1_id', //Here the column name you want to use for join
'className'=>'Animals',
'joinType' => 'INNER'
]);
EDIT(for comment query):-
If you want to select other tables, you can achieve this in two ways
In controller
$this->loadModel('OtherModelName'); //This will load your model to $this object
$other_table_result = $this->OtherModelName->find('all')->toArray();
Method to use raw SQL queries,
$sql = "SELECT * FROM users";
$query = $this->connection->prepare($sql);
$query->execute();
$result = $query->fetchAll();

How to order a CakePHP query by an aggregate function on an associated table's field?

I want to order by an aggregate function on an associated table's field, but when I debug the SQL query being executed, the associated table, PersonaHistory doesn't even get JOINed, and of course I don't get any results.
Is there a way to achieve this?
Can I force a table to be joined in the query?
$query = $this->Personas->find('all')
->contain(['PersonaHistory'])
->order(['MAX(PersonaHistory.updated)' => 'ASC'])
->group('PersonaHistory.persona_id');
DB: Personas has many PersonaHistory
It looks as if Personas is related to PersonaHistory via hasMany. Only hasOne and belongsTo associations produce a JOIN statement.
A workaround is to rewrite your query as:
$query = $this->PersonaHistory->find('all')
->contain(['Personas'])
->order(['MAX(PersonaHistory.updated)' => 'ASC'])
->group('PersonaHistory.persona_id');
your table name need to be plurialized and you can execute your order and group directly on your associated table field
<?php $query = $this->Personas->find('all')->contain([
'PersonaHistories' => function ($q) {
return $q->order(['Max(updated)' => 'ASC'])
->group('persona_id');
}]);

Best rules to get data with Contain

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.

CakePHP find on a join table

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

Find conditions 'NOT EXISTS' for hasMany relationships

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

Resources