Model group and order - cakephp

I have Order model, which belogns to User model by field user_id.
Now I want to get 1 OLDEST order per each user. For example:
id = 0, user_id = 1, created = 2012-10-10 20:36:42
id = 1, user_id = 1, created = 2012-10-10 19:36:42
id = 2, user_id = 1, created = 2012-10-10 21:36:42
id = 3, user_id = 2, created = 2012-10-10 22:36:42
id = 4, user_id = 2, created = 2012-10-10 21:36:42
id = 5, user_id = 2, created = 2012-10-10 23:36:42
So result should be:
id = 1, user_id = 1, created = 2012-10-10 19:36:42
id = 4, user_id = 2, created = 2012-10-10 21:36:42
I am using following find options for now:
array(
'group' => 'user_id',
'order' => array('MIN(created) DESC'),
)
and also tried with 'order' => array('created DESC')
but it doesn't work - it seems that GROUPING orders is done always before ORDERING.
How I can force the find method to order records by created field before grouping?

You can use the HAVING clause to filter the group. To construct the query with CakePHP, you'll need to include the clause in the ORDER clause.
$this->Order->find('all', array(
'group' => 'user_id HAVING created = MIN(created)',
'order' => array('created DESC')
));

I would use CakePHP's Containable Behavior:
//User model
public $actsAs = array('Containable');
public function oldestOrderPerUser() {
$this->recursive = -1;
$this->find('all', array(
'contain' => array(
'Order' => array(
'order' => array('created asc'),
'limit' => 1
)
)
));
}
This finds all users (feel free to add 'conditions' to limit it to specific user(s), and gets the first/oldest order for each user.
Your returned data will look something like this:
0 => array(
'User' => array(
'id' => 1,
'name' => 'John Doe',
'email' => 'johndoe#awesome.com',
'Order' => array(
0 => array(
'id' => '4',
'user_id' => 1,
'created' => '2012-10-10 19:36:42'
)
)
)
1 => array(
'User' => array(
...
You can limit what fields are returned in each User and Order, set conditions on either/both, limit either/both...etc etc.
I recommend putting $this->recursive=-1; in the AppModel to set it as the default - if you do that, you don't have to specify it before each query or in each model...etc. And using anything other than -1 is bad practice anyway, so... works well.
I also set public $actsAs = array('Containable'); in my AppModel.

I can't test it now, but try this way :
$this->Order->find('all', array(
'fields' => array('MIN(Order.Created) as min_created'),
'group' => 'user_id',
'order' => array('min_created DESC')
));

Related

CakePHP is linking models locally, but not on hosted copy?

I have employees who have one employer. In my employees table I have a field called employer_id. A report using this information is working on my local development setup, but not on my web hosting. I can't figure out what might be causing it not to.
When I try to pull up a list of all the employees I get the following error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'Employer.last_name' in 'where clause'
SQL Query: SELECT Employee.id, Employee.employer_id,
Employee.first_name, Employee.last_name, Employee.email,
Employee.webinar_id, Employee.questions,
Employee.correct_answers, Employee.required_to_pass,
Employee.status, Employee.role, Employee.sport,
Employee.program, Employee.created, Employee.modified FROM
sportrisk_wp.employees AS Employee WHERE
((Employee.last_name NOT LIKE '') AND (Employer.last_name NOT
LIKE '')) AND contain = '1' ORDER BY Employee.created DESC LIMIT
50
On my local copy when I change debug to 2 I see the following in the SQL log (it's joining the Employer):
SELECT Employee.id, Employee.employer_id,
Employee.first_name, Employee.last_name, Employee.email,
Employee.webinar_id, Employee.questions,
Employee.correct_answers, Employee.required_to_pass,
Employee.status, Employee.role, Employee.sport,
Employee.program, Employee.created, Employee.modified,
Employer.id, Employer.employer_id, Employer.company,
Employer.first_name, Employer.last_name, Employer.phone,
Employer.email, Employer.username, Employer.password,
Employer.usercode, Employer.subscriber, Employer.active,
Employer.admin, Employer.created, Employer.modified,
Employer.status FROM local.employees AS Employee LEFT JOIN
local.users AS Employer ON (Employee.employer_id =
Employer.id) WHERE ((Employee.last_name NOT LIKE '') AND
(Employer.last_name NOT LIKE '')) ORDER BY Employee.created
DESC LIMIT 50
I believe this is all the relevant code.
UsersController
// inside a larger function
// Load Employee Model
$this->loadModel('Employee');
// Base conditions - to hide some old records which do not contain name information.
$conditions = array(
"Employee.last_name NOT LIKE" => '',
"Employer.last_name NOT LIKE" => ''
);
// Find records & paginate
$this->paginate = array(
'limit' => 50,
'conditions' => array(
"AND" => $conditions
),
'order' => array('Employee.created DESC')
);
$employees = $this->paginate('Employee');
?>
Employee Model
<?php
class Employee extends AppModel
{
var $name = 'Employee';
var $recursive = 1;
var $belongsTo = array (
'Employer' => array (
'className' => 'Employer',
'foreignKey' => 'employer_id',
'order' => 'Employee.last_name ASC'
)
);
public $actsAs = array('Containable');
var $validate = array(
'email' => array(
'rule' => 'notBlank'
),
'first_name' => array(
'rule' => 'notBlank'
),
'last_name' => array(
'rule' => 'notBlank'
),
'usercode' => array(
'rule' => 'notBlank'
)
);
}
?>
Employer Model
<?php
class Employer extends AppModel
{
var $name = 'Employer';
var $recursive = -1;
var $hasOne = 'Supervisor';
var $hasMany = 'Employee';
public $useTable = 'users';
public $actsAs = array('Containable');
var $validate = array(
'email' => array(
'rule' => 'notBlank'
),
'password' => array(
'rule' => 'notBlank'
)
);
}
?>
I can't figure out why it isn't linking the Employer model in one environment but is on the other. I've done a straight copy of all the files.
SQL Queries
This is the query I get on the hosted site
SELECT `Employee`.`id`, `Employee`.`employer_id`, `Employee`.`first_name`, `Employee`.`last_name`, `Employee`.`email`, `Employee`.`webinar_id`, `Employee`.`questions`, `Employee`.`correct_answers`, `Employee`.`required_to_pass`, `Employee`.`status`, `Employee`.`role`, `Employee`.`sport`, `Employee`.`program`, `Employee`.`created`, `Employee`.`modified` FROM `sportrisk_wp`.`employees` AS `Employee` WHERE ((`Employee`.`last_name` NOT LIKE '') AND (`Employer`.`last_name` NOT LIKE '')) ORDER BY `Employee`.`created` DESC LIMIT 50
This is what I get on my local site (and what I'm looking for)
SELECT `Employee`.`id`, `Employee`.`employer_id`, `Employee`.`first_name`, `Employee`.`last_name`, `Employee`.`email`, `Employee`.`webinar_id`, `Employee`.`questions`, `Employee`.`correct_answers`, `Employee`.`required_to_pass`, `Employee`.`status`, `Employee`.`role`, `Employee`.`sport`, `Employee`.`program`, `Employee`.`created`, `Employee`.`modified`, `Employer`.`id`, `Employer`.`employer_id`, `Employer`.`company`, `Employer`.`first_name`, `Employer`.`last_name`, `Employer`.`phone`, `Employer`.`email`, `Employer`.`username`, `Employer`.`password`, `Employer`.`usercode`, `Employer`.`subscriber`, `Employer`.`active`, `Employer`.`admin`, `Employer`.`created`, `Employer`.`modified`, `Employer`.`status` FROM `local`.`employees` AS `Employee` LEFT JOIN `local`.`users` AS `Employer` ON (`Employee`.`employer_id` = `Employer`.`id`) WHERE ((`Employee`.`last_name` NOT LIKE '') AND (`Employer`.`last_name` NOT LIKE '')) ORDER BY `Employee`.`created` DESC LIMIT 50
END RESULT
In the end (after a chat with Ved) my paginate array ended up being this, and my report loads.
$this->paginate = array(
'contain' => array('Employer'),
'limit' => 50,
'conditions' => array(
"AND" => $conditions
),
'joins' => array( array( 'alias' => 'Employer', 'table' => 'users', 'type' => 'LEFT', 'conditions' => 'Employer.id = Employee.employer_id' ) ),
'fields' => array('Employee.first_name', 'Employee.last_name', 'Employee.email', 'Employee.webinar_id', 'Employee.questions', 'Employee.correct_answers', 'Employee.required_to_pass', 'Employee.status', 'Employee.role', 'Employee.sport', 'Employee.program', 'Employee.created', 'Employer.id', 'Employer.employer_id', 'Employer.company', 'Employer.first_name', 'Employer.last_name', 'Employer.email', 'Employer.active'),
'order' => array('Employee.created DESC')
);
I think you have forget to load associated data, use contain
// Find records & paginate
$this->paginate = array(
'contain' => array('Employer'),
'fields' => array('Employer.id'), // add more fields
'limit' => 50,
'conditions' => array(
"AND" => $conditions
),
'joins' => array( array( 'alias' => 'Employer', 'table' => 'users', 'type'
=> 'LEFT', 'conditions' => 'Employer.id = Employee.employer_id' ) ),
'order' => array('Employee.created DESC')
);

Cakephp find query not working

I am trying to make a query using cakephp model binding. Getting an unexpected result. But whenever I run the query in phpmyadmin it works fine. Getting this error
Column not found: 1054 Unknown column 'Comment.post_id' in 'field list'
Can't figure out what is wrong
PostsController
$posts = $this->Post->find('all', array(
'conditions' => array(
'Post.is_deleted' => 0,
'Post.is_active' => 1,
),
'fields' => array(
'Post.post_id,Post.post_title,Post.post_body,User.username,Post.added_time',
'Product.product_title','COUNT(Comment.post_id) AS total_comment'
),
'GROUP'=>'Post.post_id',
'order' => 'added_time DESC',
));
Post Model
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreign_key' => 'post_id'
),
'Product' => array(
'className' => 'Product',
'foreign_key' => 'post_id'
)
);
public $hasMany = array(
'Comment'=>array(
'className'=>'Comment',
'foreign_key'=>'post_id'
)
);
Comment Model
public $belongsTo = array(
'Post'=>array(
'className'=>'Post',
'foreign_key'=>'post_id'
)
);
My expected query
SELECT posts.`post_title`, users.username,products.product_title, posts.post_body, COUNT(comments.post_id) as total_comments from posts LEFT JOIN comments on comments.post_id = posts.post_id LEFT join users on users.user_id = posts.user_id LEFT JOIN products on products.product_id = posts.product_id WHERE posts.is_active = 1 and posts.is_deleted = 0 GROUP BY posts.post_id
Try by setting the recursive level 1 or 2
$this->Post->find('all', array(
'conditions' => array(
'Post.is_deleted' => 0,
'Post.is_active' => 1,
),
'fields' => array(
'Post.post_id,Post.post_title,Post.post_body,User.username,Post.added_time',
'Product.product_title','COUNT(Comment.post_id) AS total_comment'
),
'GROUP'=>'Post.post_id',
'order' => 'added_time DESC',
'recursive'=>1
));

CakePHP: How to make the paginator component use distinct counting?

I am making simple pagination which using this code:
$paginate = array(
'limit' => 30,
'fields' => array('DISTINCT Doctor.id','Doctor.*'),
'order' => array('Doctor.id' => 'desc'),
'joins' => array(
array('table' => 'doctors_medical_degrees',
'alias' => 'DoctorsMedicalDegree',
'type' => 'INNER',
'conditions' => array(
'Doctor.id = DoctorsMedicalDegree.doctor_id',
)
),
),
'recursive' => -1,
);
$this->Paginator->settings = $paginate;
$data = $this->Paginator->paginate('Doctor');
Now the problem is I am using Inner join so for Distinct result I am using Distinct Doctor.id, but the cakephp when doing query for pagination the count query not including Distinct Doctor.id
'query' => 'SELECT COUNT(*) AS `count` FROM `pharma`.`doctors` AS `Doctor` INNER JOIN `pharma`.`doctors_medical_degrees` AS `DoctorsMedicalDegree` ON (`Doctor`.`id` = `DoctorsMedicalDegree`.`doctor_id`)'
as you can see No
COUNT(DISTINCT Doctor.id)
so pagination return more number of result which it can actually return for
The problem is that the paginator doesn't pass the fields to the find('count') call, so by default it will always count on *.
But even if it would pass the fields, passing an array would make the find('count') call expect that the field to count is passed as a COUNT() expression, ie something like
'fields' => array('COUNT(DISTINCT Doctor.id) as `count`')
However that won't work with the paginator anyways, so what you need is a customized find('count') call.
Custom query pagination to the rescue
See Cookbook > Pagination > Custom Query Pagination for more information.
Custom query pagination is probably your best bet, that way it's totally up to you how counting is being done.
For example you could make use of the extra values passed by the paginator component, that way you could pass the field to count on to the find('count')` call, something like this (untested example code):
class Doctor extends AppModel {
// ...
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions');
if($recursive != $this->recursive) {
$parameters['recursive'] = $recursive;
}
if(!empty($extra['countField'])) {
$parameters['fields'] = $extra['countField'];
unset($extra['countField']);
}
return $this->find('count', array_merge($parameters, $extra));
}
}
$this->Paginator->settings = array(
'limit' => 30,
'fields' => array('DISTINCT Doctor.id','Doctor.*'),
// ...
'countField' => 'DISTINCT Doctor.id'
);
$data = $this->Paginator->paginate('Doctor');
This should then create a COUNT query that looks like
SELECT COUNT(DISTINCT Doctor.id) AS `count` ...
I found the solution on CakePHP - Pagination total count differs from actual count when using DISTINCT
Add the public function paginateCount in your model and use the distinct option in your paginator like:
$this->paginate = array('limit' => $limit, 'order' => array('Item.created' => 'ASC', 'Item.code' => 'ASC'),
'fields' => 'DISTINCT Item.*',
'conditions' => $conditions,
'countField' => array('Item.id'),
'joins' => $joins,
'distinct' => 'Item.id',
'contain' => array(
'Image' => array('limit' => 1),
'ItemsTag'
)
);

CakePHP runs unnecessary queries when retrieving related models

The Event model has following relations:
var $belongsTo = array(
'Project' => array(
'className' => 'Project',
'foreignKey' => 'project_id',
),
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
here is how I list the events depending on the user input:
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array(
'conditions'=>$conditions, 'order' => array('Event.date' => 'asc')));
And here are the 3 queries that are being run:
1 SELECT `User`.`id`, `User`.`name`, `User`.`surname` FROM `scheduling`.`users` AS `User` WHERE `company_id` = 1
2 SELECT `Project`.`id`, `Project`.`name` FROM `scheduling`.`projects` AS `Project` LEFT JOIN `scheduling`.`customers` AS `Customer` ON (`Project`.`customer_id` = `Customer`.`id`) WHERE `Project`.`company_id` = 1
3 SELECT `Event`.`id`, `Event`.`project_id`, `Event`.`user_id`, `Event`.`date`, `Event`.`hours`, `Event`.`minutes`, `Event`.`xhours`, `Event`.`xminutes`, `Event`.`xdetails`, `Event`.`assignment`, `Event`.`start_time`, `Event`.`material`, `Event`.`meter_drive`, `Event`.`time_drive`, `Event`.`start_location`, `Event`.`finish_time`, `Project`.`id`, `Project`.`name`, `Project`.`customer_id`, `Project`.`project_nr`, `Project`.`address`, `Project`.`post_nr`, `Project`.`city`, `Project`.`company_id`, `Project`.`color`, `Project`.`start_date`, `Project`.`finish_date`, `User`.`id`, `User`.`employee_nr`, `User`.`name`, `User`.`surname`, `User`.`email`, `User`.`password`, `User`.`role`, `User`.`phone`, `User`.`address`, `User`.`post_nr`, `User`.`city`, `User`.`token_hash`, `User`.`company_id`, `User`.`car_id`, `User`.`image` FROM `scheduling`.`events` AS `Event` LEFT JOIN `scheduling`.`projects` AS `Project` ON (`Event`.`project_id` = `Project`.`id`) LEFT JOIN `scheduling`.`users` AS `User` ON (`Event`.`user_id` = `User`.`id`) WHERE `user_id` = 1 AND `project_id` = 5 AND `date` >= '2013-07-01' AND `date` <= '2013-12-06' ORDER BY `Event`.`date` asc
In fact, I only need the third query and not the first two. What causes them and how to get rid of them?
By default, CakePHP will try attach associations.
In all 3 models add:
var $actsAs = array('Containable');
This will allow you to "contain" your queries to specific models.
Now you can do you query thus:
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'contain' => true));
Say you did want Users (but not Projects) back you can do:
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'contain' => array('User')));
Other code is responsible
This code:
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc')
));
Is responsible for this query:
SELECT
`Event`.`id`,
...
FROM
`scheduling`.`events` AS `Event`
LEFT JOIN
`scheduling`.`projects` AS `Project` ON (`Event`.`project_id` = `Project`.`id`)
LEFT JOIN
`scheduling`.`users` AS `User` ON (`Event`.`user_id` = `User`.`id`)
WHERE
`user_id` = 1 AND
`project_id` = 5 AND
`date` >= '2013-07-01' AND
`date` <= '2013-12-06'
ORDER BY
`Event`.`date` asc
However there are only user_id and date conditions in the code in the question.
This condition:
`project_id` = 5
Is being added by un-shown code - probably a behavior. Check your code for where the project_id condition is defined, there is the answer.
query #1 is unrelated
The first query does not look to be related to the code in the question at all - there is nothing in the question that requires finding a user's data. To find where that's coming from - you can use a simple technique. Open up the user model and put this in it:
class User extends AppModel {
public function beforeFind() {
debug(Debugger::trace());
debug(func_get_args());
die;
}
}
This will give a stack trace of how the query is being triggered - edit the application code appropriately once you know where it comes from.
query #2 is required
Assuming the query you want is actually correct (find all events for a single project) - there needs to be a way to restrict on project id. If that's not specified explicitly, the second query is looking for a project id by client id - i.e. the query you want depends upon that data.
This will help you.
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'recursive' => -1
));
recursive based on the max containment depth
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array('recursive'=>0,
'conditions'=>$conditions, 'order' => array('Event.date' => 'asc')));
the change in here is the recursive
recursive=>-1 mean will fetch only event
recursive=>0 mean will fetch event + to whom it belongs to, in this case project and user

cakephp 'contain' left joint does not use specified foreignKey for left join

On cakephp 2.1, I have two tables: qca belongs to employee via field emp_number on both tables.
qca model belongsTo : (pleae note foreignKey)
public $actsAs = array('Containable');
var $belongsTo = array('Dir',
'Employee' => array(
'className' => 'Employee',
'foreignKey' => 'emp_number')
);
employee model:
public $actsAs = array('Containable');
On my controller's find, i use 'contain' to retrieve employee info based on emp_number from qca table.
$hoursvalues = $this->Qca->find('all', array('conditions' => $conditions,
'fields' => array('Qca.emp_number', 'Sum(CASE WHEN Qca.qca_tipcode = 1 THEN 1 END) AS Qca__comps', 'Sum(qca_end - qca_start) as Qca__production', 'Sum(Qca.qca_durend) as Qca__idle'),
'contain' => array(
'Employee' => array(
'fields' => array('emp_number', 'emp_ape_pat', 'emp_ape_mat', 'emp_ape_mat'))),
'group' => array('Qca.emp_number'),
));
However, the executed sql sentence shows:
LEFT JOIN `devopm`.`employees` AS `Employee` ON (`Qca`.`emp_number` = `Employee`.`id`)
Whereas
Employee.id should be Employee.emp_number
This is the full sql sentence:
SELECT `Qca`.`emp_number`, Sum(CASE WHEN Qca.qca_tipcode = 1 THEN 1 END) AS Qca__comps, Sum(qca_end - qca_start) as Qca__production, Sum(`Qca`.`qca_durend`) as Qca__idle, `Employee`.`emp_number`, `Employee`.`emp_ape_pat`, `Employee`.`emp_ape_mat`, `Employee`.`id` FROM `devopm`.`qcas` AS `Qca` LEFT JOIN `devopm`.`employees` AS `Employee` ON (`Qca`.`emp_number` = `Employee`.`id`) WHERE `Qca`.`dir_id` = 63 AND FROM_UNIXTIME(`Qca`.`qca_start`, '%Y-%m-%d') >= '2012-07-18' AND FROM_UNIXTIME(`Qca`.`qca_start`, '%Y-%m-%d') <= '2012-07-18' GROUP BY `Qca`.`emp_number`
This results on null values returned for Employee:
array(
(int) 0 => array(
'Qca' => array(
'emp_number' => 'id3108',
'comps' => '2',
'production' => '7784',
'idle' => '529'
),
'Employee' => array(
'emp_ape_pat' => null,
'emp_ape_mat' => null,
'id' => null
)
),
Note: I have other instance of 'contain' working (one with default id = tableName.id). I'm wondering if the foreignKey on belongsTo ('foreignKey' => 'emp_number') is just not good for 'contain' to work?
Can you help?
Thank you so much.
(I found a workaround but slows down the query a great deal (it duplicates the left join and takes forever)
$joins = array(
array('table' => 'publication_numerations',
'alias' => 'PublicationNumeration',
'type' => 'LEFT',
'conditions' => array(
'Publication.id = PublicationNumeration.publication_id',
)
)
);
$this->Publication->find('all', array('joins' => $joins));

Resources