CakePHP counterCache joining irrelevant tables to update counter - mysql

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);

Related

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());
}

How to use conditions in self association without joins?

I have Product model [id, parent_id, x].
I want to append on each Product, single ProductParent, only when Product.x = 0, and Product.parent_id = ProductParent.id.
What i try is:
public $hasOne = array(
'ProductParent' => array(
'className' => 'Product',
'foreignKey' => false,
'conditions' => array(
'ProductParent.id = Product.parent_id',
'Product.x' => 0
)
)
);
And get output Product.parent_id is unknown.
You can try using Tree behavior. Check this: http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html

Convert the MySql query to CakePhp 2.0

How do I convert this MySql query to CakePhp's find.
And please tell me how can i practice writing Find queries in cakephp
select distinct trips.fk_userid from spots, trips
where spots.fk_tripid = trips.id
and trips.isapproved = 1
and spots.id in (".$row[$first_index]['spot_list'].")
The model can be Trip and you can query like this
$this->Trip->query("select distinct trips.fk_userid from spots, trips where spots.fk_tripid = trips.id and trips.isapproved = 1 and spots.id in (".$row[$first_index]['spot_list'].")");
or
You should create Trip and Spot model and in Trip model, you have to have this in Spot model:
public $belongsTo = array(
'Trip' => array(
'className' => 'Trip',
'foreignKey' => 'fk_tripid'
)
);
and query it like this:
$this->Spot->find('all', array(
'fields' => array("distinct Trip.fk_userid"),
'conditions' => array(
'Trip.isapproved' => 1,
'Spot.id' => $row[$first_index]['spot_list']
)
));

ISNULL not working in order in CAKEPHP

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.

CakePHP model associations - Random order on retrieve of associated model for HABTM

I am retrieving data:
$mydata = $this->ProductList->find('all', array('order' => 'rand()', 'conditions' => array('name' => 'we love')));
I have set up a HABTM relationship to the Product model. As you can see, I am fetching all products in the 'we love'-list. Now, I want those Products I am retrieving to be randomised. But they are not, instead the MySQL is randomised on the ProductList model as you can see in the SQL. Why is that? How can I get the random fetch on the Products instead?
Resulting MySQL query:
SELECT `ProductList`.`id`, `ProductList`.`name` FROM `database`.`product_lists` AS `ProductList` WHERE `name` = 'we love' ORDER BY rand() ASC
SELECT `Product`.`id`, `Product`.`category_id`, `Product`.`name`, `Product`.`price`, `Product`.`description`, `ProductListsProduct`.`product_list_id`, `ProductListsProduct`.`product_id` FROM `database`.`products` AS `Product` JOIN `database`.`product_lists_products` AS `ProductListsProduct` ON (`ProductListsProduct`.`product_list_id` = 3 AND `ProductListsProduct`.`product_id` = `Product`.`id`)
EDIT:
There are so many different ways to approach this; to get a random product from a user's product list. You could do it with PHP - just find all of the products and then use rand() to pick on from the returned array. You could set a Model query condition. The list goes on...
I would probably create an alias to the Product model in ProductList called RandomProduct. You could set the query for the retrieved product when you set the relationship:
public $hasMany = array(
'RandomProduct' => array(
'className' => 'Product',
'foreignKey' => 'product_list_id',
'order' => 'Rand()',
'limit' => '1',
'dependent' => true
)
);
You can then use the containable behavior so that this model is only retrieved when you need it. (You wouldn't need to do this if recursive finds are greater than -1, but I usually do that as best practice so that my models only query for the data that they need.) The following would return any ProductList called 'we love' and a "random" product associated with that list.
$mydata = $this->ProductList->find(
'all',
array(
'conditions' => array(
'name' => 'we love'
)
),
'contain' => array(
'RandomProduct'
)
);