ISNULL not working in order in CAKEPHP - mysql

I am using LEFT join and as a result getting null values for is_read column in Messages table. I want to keep the nulls at bottom when ordering. I'm using this in paginator. Following is the my code for doing the same:
$this->Paginator->settings = array(
'fields' => array('User.*'),
'joins' => array(
array('table' => 'messages',
'alias' => 'Message',
'type' => 'LEFT',
'conditions' => array(
'User.id = Message.user_from_id'
)
),
),
'limit' => 20,
'group' => array('User.id'),
'order' => array('ISNULL(Message.is_read)' => 'asc','Message.is_read' => 'asc', 'Message.created' => 'asc'),
);
The query Cakephp generates for this is as follows:
SELECT `User`.*, (CONCAT(`User`.`first_name`, ' ', `User`.`last_name`)) AS `User__full_name` FROM `srs_development`.`users` AS `User` LEFT JOIN `srs_development`.`messages` AS `Message` ON (`User`.`id` = `Message`.`user_from_id`) WHERE 1 = 1 GROUP BY `User`.`id` ORDER BY `Message`.`is_read` asc, `Message`.`created` asc LIMIT 20
ISNULL function is getting omitted in the final query.
Also please suggest a way to accomplish this without using custom pagination() if possible.

Aggregate functions didn't work in the order clause when using Pagination component. I tried declaring a virtual field in Message model as:
public $virtualFields = array(
'sortme' => "ISNULL(Message.is_read)",
);
So finally, declaring it as virtual field in the Message model did the job.
Thank you everyone.

Related

how to use straight_join in cakephp?

I write codes as seen below when joining two and more tables in cakephp:
$options['joins'] = array(
array(
'table' => 'table2',
'alias' => 'tbl2',
'type' => 'inner',
'conditions' => array(
'tbl1.customer_id = tbl2.customer_id' ,
"tbl2.del_flag = 'N'" ,
)
)
);
That continues with more tables included in the join.
How do I write the code if I wanna use 'straight_join' instead of 'inner join'?
Changing the 'type' above into 'straight' doesn't do anything.
Is it possible in cakephp?
Thank you for the help.

CakePHP Sort order not affecting all records

In my CakePHP 2.7.7 app, I have an issue where using PaginatorComponent isn't properly sorting my results. See this link for an image:
Sample Data
The data should be sorted by descending last name, but you can see there are a couple users who are seemingly exempt from this data. Doesn't matter what order I do it in, ascending or descending, these few records don't get sorted. For reference:
index() in UsersController:
public function index() {
$this->User->contain('Status');
$this->paginate = array(
'limit' => 15,
'order' => array('User.last_name' => 'DESC'),
'conditions' => array('User.department_id =' => $this->Session->read('Auth.User.department_id')),
'contain' => 'Status'
);
$this->set('users', $this->Paginator->paginate());
}
Any ideas what could have caused this? I'm at a loss here. Thanks in advance!
SELECT `User`.`id`, `User`.`first_name`, `User`.`last_name`, `User`.`middle`, `User`.`address`, `User`.`address_2`, `User`.`city`, `User`.`state_id`, `User`.`zip`, `User`.`home`, `User`.`cell`, `User`.`work`, `User`.`email`, `User`.`status_id`, `User`.`status_reason`, `User`.`rank`, `User`.`birthday`, `User`.`gender`, `User`.`school`, `User`.`employer`, `User`.`position`, `User`.`created`, `User`.`modified`, `User`.`updated_by`, `User`.`radio`, `User`.`ident`, `User`.`parent_name`, `User`.`parent_number`, `User`.`parent_email`, `User`.`squad`, `User`.`squad_leader`, `User`.`ride_along`, `User`.`drivers_license`, `User`.`username`, `User`.`password`, `User`.`department_id`, `User`.`join_date`, `User`.`group_id`, `User`.`test`, (CONCAT(`User`.`first_name`, " ", `User`.`last_name`)) AS `User__name`, `Status`.`id`, `Status`.`status` FROM `admin_cake`.`users` AS `User` LEFT JOIN `admin_cake`.`statuses` AS `Status` ON (`User`.`status_id` = `Status`.`id`) WHERE `User`.`department_id` = 1 ORDER BY `User`.`last_name` ASC LIMIT 15
When you use $this->paginate to set conditions, limit...you need to use function $this->pagination().
$this->pagination and $this->Paginator->pagination() can not be combined.
1)
public function index() {
$this->User->contain('Status');
$this->paginate = array(
'limit' => 15,
'order' => array('User.last_name' => 'DESC'),
'conditions' => array('User.department_id =' => $this->Session->read('Auth.User.department_id')),
'contain' => 'Status'
);
// Assign settings
$this->Paginator->settings = $this->paginate;
$this->set('users', $this->Paginator->paginate());
}
2)
public function index() {
$this->User->contain('Status');
$this->paginate = array(
'limit' => 15,
'order' => array('User.last_name' => 'DESC'),
'conditions' => array('User.department_id =' => $this->Session->read('Auth.User.department_id')),
'contain' => 'Status'
);
//Don't use $this->Paginator->paginate()
$this->set('users', $this->paginate());
}

cakephp set condition on MAX()

I need max submitted orders in the last 30 days. max field is a virtualField.
public $virtualFields = array(
'max_submitted' => "MAX(`WorkRecord`.`submitted`)"
);
I get error #1054 - Unknown column 'WorkRecord.max_submitted' in 'field list'
SELECT `WorkRecord`.`id`, `Order`.`fee`, `Order`.`order_id`,
`Order`.`min_sources`, `Order`.`min_references`, `Order`.`am_level`,
`Order`.`am_standard`, `Order`.`am_type`, `Order`.`am_subject`, `Order`.`am_word_count`,
`Order`.`ref_style`, `Order`.`service`, `BriefInstalment`.`deadline`,
`BriefInstalment`.`id`, (MAX(`WorkRecord`.`submitted`)) AS `WorkRecord__max_submitted`
FROM `writers`.`work_records` AS `WorkRecord`
RIGHT JOIN `torg`.`temp_orders` AS `Order` ON (`Order`.`order_id` = `WorkRecord`.`order_id` AND `Order`.`status2` > 2 AND `Order`.`am_type` NOT LIKE '%phd%') LEFT JOIN `writers`.`brief_instalments` AS `BriefInstalment` ON (`WorkRecord`.`brief_instalment_id` = `BriefInstalment`.`id`)
WHERE `WorkRecord`.`writer_id` = 7827
AND `WorkRecord`.`withdrawn` IS NULL
AND `WorkRecord`.`max_submitted` BETWEEN NOW() - INTERVAL 30 DAY AND NOW() GROUP BY `Order`.`order_id`
You can't use aggregate function results in where clauses, because the results of the aggregate (e.g. max()) will NOT be available when the where clause is being applied.
Move the max() to a `having:
SELECT ..., max(foo) AS foo
FROM ...
WHERE ...
HAVING foo BETWEEN ...
having is basically applied as the last step just before the results are sent over the client. By that time, all of the calculations/aggregations are done.
For others. Here is what I should have done.
$workRecords = $this->find('all', array(
'fields' => array(
'WorkRecord.id', 'MAX(`WorkRecord`.`submitted`) AS submitted',
'Order.fee', 'Order.order_id', 'Order.min_sources', 'Order.min_references', 'Order.am_level', 'Order.am_standard',
'Order.am_type', 'Order.am_subject', 'Order.am_word_count', 'Order.ref_style', 'Order.service'
),
'conditions' => array(
'WorkRecord.writer_id' => $userId,
'WorkRecord.withdrawn IS NULL'
),
'joins' => array(
array(
'table' => 'torg.temp_orders',
'alias' => 'Order',
'type' => 'RIGHT',
'conditions' => array(
'Order.order_id = WorkRecord.order_id',
'Order.status2 >' => 2,
"Order.am_type NOT LIKE '%phd%'"
)
)
),
'contain' => array(
'BriefInstalment.deadline'
),
'group' => array(
'Order.order_id HAVING `submitted` BETWEEN NOW() - INTERVAL 30 DAY AND NOW()'
)
));

CakePHP counterCache joining irrelevant tables to update counter

I have a User model and a Message model.
The Message model is linked to the User model twice like this:
public $belongsTo = array(
'UserSender' => array(
'className' => 'User',
'foreignKey' => 'sender_id',
'counterCache' => array(
'messages_sent_count' => array(
'is_deleted' => FALSE
)
)
),
'UserRecipient' => array(
'className' => 'User',
'foreignKey' => 'recipient_id',
'counterCache' => array(
'messages_received_count' => array(
'is_deleted' => FALSE
),
'messages_unread_count' => array(
'is_deleted' => FALSE,
'is_read' => FALSE
)
)
),
'Operator' => array(
'className' => 'Operator',
'foreignKey' => 'operator_id'
)
);
Besides the User model, the Message model also $belongsTo the Operator model. The Operator model is irrelevant to the message count for the users, but its table is still being joined in the count query, as debug shows:
'query' => 'SELECT COUNT(*) AS `count` FROM `database`.`messages` AS `Message` LEFT JOIN `database`.`operators` AS `Operator` ON (`Message`.`operator_id` = `Operator`.`id`) LEFT JOIN `database`.`users` AS `UserSender` ON (`Message`.`sender_id` = `UserSender`.`id`) LEFT JOIN `database`.`users` AS `UserRecipient` ON (`Message`.`recipient_id` = `UserRecipient`.`id`) WHERE `Message`.`is_deleted` = '0' AND `Message`.`sender_id` = 389',
'params' => array(),
'affected' => (int) 1,
'numRows' => (int) 1,
'took' => (float) 394
For the sake of simplicity I've actually excluded one more model that the Message model $belongsTo, but the above query shows the problem.
The counterCache function does a quite expensive query just to update the counter. Is there a way to maybe override or adjust the counterCache method to not join irrelevant tables in the query?
I can't test it right now, but since the recursive setting used by Model::updateCounterCache() is hard-coded based on whether conditions are defined for the counter cache field, the only way to change this (besides completely reimplementing Model::updateCounterCache()) is probably to modify the count query in Model::_findCount() or Model::beforeFind() of your Message model.
public function beforeFind($query) {
// ... figure whether this is the count query for updateCounterCache,
// maybe even try to analyze whether the passed conditions require
// joins or not.
if(/* ... */) {
$query['recursive'] = -1;
}
return $query;
}
Depending on how much control you'll actually need the containable behavior might do the trick too, it sets recursive to -1 in case no containments are being passed
$Message->contain(); // === recursive is being set to -1 in before find callback
$Message->delete(123);

CakePHP - Virtual field issue using Paginator Helper

I have a Listings table with lat/long fields. I'm using the Haversine Formula to calculate the distance (as an alias/virtual field) between an origin point (33.987339, -81.036819) and the lat/long of each Listing and returning the listings with a distance within 10 miles of the origin point.
The following SQL query in phpMyAdmin returns exactly what I expect:
SELECT *, round(3959 * acos(cos(radians(33.987339)) * cos(radians(Listing.lat)) * cos(radians(Listing.long) - radians(-81.036819)) + sin( radians(33.987339)) * sin(radians(Listing.lat))))
AS distance, `Listing`.`id`
FROM `preview_site`.`listings` AS `Listing`
LEFT JOIN `preview_site`.`users` AS `User` ON (`Listing`.`user_id` = `User`.`id`)
LEFT JOIN `preview_site`.`categories` AS `Category` ON (`Listing`.`category_id` = `Category`.`id`)
LEFT JOIN `preview_site`.`states` AS `State` ON (`Listing`.`state_id` = `State`.`id`)
WHERE `Listing`.`status` = 'Active'
HAVING distance < 10
ORDER BY `distance` ASC LIMIT 20
After attempting (and failing several ways) to get the CakePHP code to correctly generate the above SQL, I used this tool to generate the following CakePHP controller code (it gave both Model and Controller options) from the SQL:
$this->Paginator->virtualFields = array(
'distance' => 'round(3959 * acos(cos(radians(33.987339)) * cos(radians(Listing.lat )) * cos(radians(Listing.long) - radians(-81.036819)) + sin(radians(33.987339)) * sin(radians(Listing.lat))))');
$this->Paginator->settings = array(
'fields' => array(
'Listing.*',
'Listing.distance',
'Listing.id',
'Category.*',
'State.*',
'User.*',
),
'joins' => array(
array(
'conditions' => array(
'Listing.user_id = UserJoin.id',
),
'table' => 'users',
'alias' => 'UserJoin',
'type' => 'left',
),
array(
'conditions' => array(
'Listing.category_id = CatJoin.id',
),
'table' => 'categories',
'alias' => 'CatJoin',
'type' => 'left',
),
array(
'conditions' => array(
'Listing.state_id = StateJoin.id',
),
'table' => 'states',
'alias' => 'StateJoin',
'type' => 'left',
),
),
'conditions' => array(
'Listing.status' => 'Active',
),
'order' => array(
'distance' => 'asc',
),
'limit' => '5',
'having' => array(
'distance <' => '10',
),
'contain' => array(
'User',
'Category',
'State',
),
);
$data = $this->Paginator->paginate('Listing');
$this->set('listings', $data);
If I use this code, I get the following error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Listing.distance' in 'field list'
If I change $this->Paginator->virtualFields to $this->Listing->virtualFields (as I could not find any documentation on Paginator actually using the virtualFields method), I don't get any errors and the pagination works fine, but the returned results are not limited by the distance (all Listing records are returned). Here's a snippet of the generated SQL with the distance alias:
SELECT `Listing`.*, `Listing`.`id`, `Category`.*, `State`.*, `User`.*, (round(3959 * acos(cos(radians(33.987339)) * cos(radians(`Listing`.`lat` )) * cos(radians(`Listing`.`long`) - radians(-81.036819)) + sin(radians(33.987339)) * sin(radians(`Listing`.`lat`)))))
AS `Listing__distance`
FROM `preview_site`.`listings` AS `Listing`
Does anyone have any suggestions for how to make this work correctly? ANY help would be greatly appreciated.
I think where your problem is coming from is CakePHP does not recognize "Having", I believe. Since you don't seem to have a Group By, you can just use a regular WHERE and get the same results, in this case, array('conditions' => array('distance <' => 10)) If you do have a Group By though, see the below:
CakePHP: How can I use a "HAVING" operation when building queries with find method?
There are quite a few open tickets regarding virtual fields.
This might well be one of them.
Even though your initial binding to paginator looks off.
You should add virtual fields to the current model, so Listing.
$this->Listing->virtualFields['distance'] = ...
For me, in those scenarios where I could not easily use the virtual field, it helped to manually use the aliased field, so Listing__distance ASC in your order or more importantly in your having clause.
It will also reuse the already calculated field instead of doing it again (even though I don't know if there is a speed improvement here this way). See this.
Also note that it might be cleaner to leverage a behavior to avoid repeating that for other queries (and to keep it DRY):
$this->Listing->setDistanceAsVirtualField($lag, $lng);
And I usually use conditions to limit the distance (no need for having, is there?).