CakePHP - Pagination total count differs from actual count when using DISTINCT - php

I have a find method that uses a DISTINCT clause to get results from my Model. The Controller code looks like below
$options = array(
'limit' => 10,
'fields' => array(
'DISTINCT id', 'title',
),
'contain' => array(
'Dealer' => array('id'),
),
'paramType' => 'querystring'
);
$this->Paginator->settings = $options;
$cars = $this->Paginator->paginate('Car'); // returns 6 distinct rows
The above query return 6 distinct rows and 12 total rows. So when I am displaying, the screen shows 6 distinct rows
However in the View, when I use
echo $this->Paginator->param('count'); // returns 12
I get a count of 12
I checked the SQL Log and noticed that the count query is not using the distinct clause. Any idea how I can override the Paginator count query to use the DISTINCT clause?

Found the solution,
In controller add distinct as an array parameter with other pagination options. So if I was trying to retrieve a list of Cars in my inventory with 10 cars at a time, the options would have a DISTINCT clause in the fields parameter and a separate parameter called distinct would also be added as shown below
$options = array(
'conditions' => $conditions,
'joins' => $joins,
'limit' => 10,
'fields' => array(
'DISTINCT Car.id', 'title', 'user_id'),
'contain' => array(
'Dealer' => array('id'),
),
'paramType' => 'querystring',
'distinct' => 'Car.id'
);
$this->Paginator->settings = $options;
$cars = $this->Paginator->paginate('Car');
In Model, use the below function to override the original paginateCount method
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions', 'recursive');
if (isset($extra['distinct'])) {
$parameters['fields'] = 'DISTINCT ' . $extra['distinct'];
$count = $this->find('count', array_merge($parameters, $extra));
} else {
// regular pagination
$count = $this->find('count', array_merge($parameters, $extra));
}
return $count;
}
No change in View

Related

Include single item from related table

I have a table called items and a table called item_pics.
item_pics has an item_id, file_name and a rank field (among others).
What I'm looking for is for each item my index page's $items array to contain the file_name from the item_pics matching the item's item_id with the lowest rank. So I can access like (or something like) this in my Items/index.ctp:
foreach ($items as $item):
$img = $item['Item']['ItemPic']['file_name'];
...
I'm pretty new to CakePHP, this is my first project. I thought that this within the Item model would cause item_pics data to be pulled (although I figured all related item_pics for each item would get pulled rather than just the one with the lowest rank):
public $hasMany = array(
'ItemPic' => array(
'className' => 'ItemPic',
'foreignKey' => 'item_id',
'dependent' => false
)
}
but I can see that no item_pics data is loaded (at the bottom of items/index):
SELECT `Item`.`id`, `Item`.`title`, `Item`.`description`, `Item`.`created`, `Item`.`modified`, `Item`.`type`, `Project`.`id`, `Project`.`item_id`, `Project`.`title`, `Project`.`description`, `Project`.`rank`, `Project`.`created`, `Project`.`modified`
FROM `laurensabc`.`items` AS `Item`
LEFT JOIN `laurensabc`.`projects`
AS `Project`
ON (`Project`.`item_id` = `Item`.`id`)
WHERE `Item`.`type` IN (1, 2)
LIMIT 20
also, while I would like projects to be joined in the view pages, I don't really need them in the index page.
I've done some searching and haven't been able to find exactly what I'm looking for. I suppose I could do a query within the index view item loop, but I'm trying to make sure I do things the right way... the CakePHP way. I assume I need to change something about my model relationships but I haven't had any luck.
CakePHP - Associations - HasMany, this makes it seem like I could order by rank and limit 1. But this didn't work... and even if it did, I wouldn't want that to affect the view pages but rather just the index page.
My Controller looks like this:
public function index($type = null) {
$this->Item->recursive = 0;
$conditions = array();
if ($type == "sale") {
$conditions = array(
"Item.type" => array(self::FOR_SALE, self::FOR_SALE_OR_RENT)
);
} else if ($type == "rent" ) {
$conditions = array(
"Item.type" => array(self::FOR_RENT, self::FOR_SALE_OR_RENT)
);
} else {
$conditions = array("Item.type !=" => self::HIDDEN);
}
$paginated = $this->Paginator->paginate($conditions);
debug($paginated);
$this->set('items', $paginated);
$this->set('title', ($type == null ? "Items for Sale or Rent" : "Items for " . ucwords($type)));
}
I have also tried this on my controller, but it doesn't seem to do anything either:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'alias' => 'ItemPic',
'table' => 'item_pics',
'type' => 'left',
'conditions' => array('ItemPic.item_id' => 'Item.id'),
'order' => array('ItemPic.rank' => 'asc'),
'limit' => 1
)
)
);
$paginated = $this->paginate($this->Item);
First, set containable behavior in AppModel (or if you don't want it on each model, put it on Item model):
public $actsAs = array('Containable');
Then, on your find query:
$items = $this->Item->find('all', array(
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
));
Then the result array you can access it like:
foreach ($items as $item):
$img = $item['ItemPic']['file_name'];
Edit: Then you should put it on the paginate query:
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
);
In this case, I would probably order by rank and limit 1 as you said, and make that a dynamic association just for the index page (See http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly). So use $this->Item->bindModel(array('hasMany' => array('ItemPic' => $options))); (which I believe should replace your current settings for HasMany ItemPic, but you may have to unbindmodel first)
Associations created through bindModel will go through for the next query only, then it'll revert to your normal settings, unless you specifically set an option to keep using the new association.
As for why it's not getting ItemPics with Items, or why trying to order by rank and limit 1 didn't work for you, I can't really say without seeing more of your code.

CakePHP this->paginate not using order field

No matter what I do I can't get it to respect the order I specify.
$this->paginate = array(
'Car' => array(
'limit' => 6,
'order' => array(
'Car.year' => 'desc'
),
'table' => 'cars'
)
);
Generated SQL:
SELECT `Car`.`id`, ... `Car`.`year`,... FROM `cars` AS `Car` WHERE 1 = 1 LIMIT 6
Turns out I was using a :sort in my url parameters. Once I took that out all was well :)
If you only have one item, you don't need/shouldn't use an array:
//one thing
var $order = "Model.field DESC";
//multiple things
var $order = array("Model.field" => "asc", "Model.field2" => "DESC");
(per this page)
Note too that virtual fields are ignored by default when paginating.
See "Control which fields used for ordering" in Cake 2.0 Pagination documentation.
Example:
$this->MyModel->virtualFields['count'] = 0;
$this->Paginator->settings = array(
'fields' => 'COUNT(id) AS MyModel__count',
'group' => ('MyModel.group_id'),
'order' => array('MyModel__count' => 'DESC'),
);
// IMPORTANT: pass sortable fields including the virtual field as 3rd parameter:
$log = $this->Paginator->paginate('MyModel', null, array('MyModel__count', 'id'));

CakePHP Pagination count not matching query?

I've got a fairly modified pagination query using a number of Joins etc - but for some reason the paginator->counter() never matches the results from the count query.
You can see it in action at http://dev.qreer.com/ - by choosing various options on the LHS navigation, the query output is below and the paginator count appears to be pretty random.
Any idea where I can start looking to debug this?
In the Jobs Controller:
$this->paginate = $this->Job->paginateParams($data);
$jobs = $this->paginate('Job');
$this->set(compact('jobs'));
In the Model:
function paginateParams($data = null){
//lots of joins + conditions
return array('contain' => $contain, 'recursive' => 0,'joins' => $joins, 'conditions' => $conditions, 'group' => 'Job.id', 'order' => $order);
}
Sample Join (there's inner joins for all the join tables and data tables):
array(
'table' => 'education_backgrounds',
'alias' => 'EducationBackground',
'type' => 'INNER',
'conditions' => array('EducationBackgroundsJobs.education_background_id = EducationBackground.id'),
),
Sample Condition:
'EducationBackground.name' => array('Aerospace Engineering');
It's because of the group by I found a workaround. I'd love to put the link but i've lost it, so i'll post the code:
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions', 'recursive');
if (isset($extra['group'])) {
$parameters['fields'] = $extra['group'];
if (is_string($parameters['fields'])) {
// pagination with single GROUP BY field
if (substr($parameters['fields'], 0, 9) != 'DISTINCT ') {
$parameters['fields'] = 'DISTINCT ' . $parameters['fields'];
}
unset($extra['group']);
$count = $this->find('count', array_merge($parameters, $extra));
} else {
// resort to inefficient method for multiple GROUP BY fields
$count = $this->find('count', array_merge($parameters, $extra));
$count = $this->getAffectedRows();
}
} else {
// regular pagination
$count = $this->find('count', array_merge($parameters, $extra));
}
return $count;
}
I added it in the app_model and it works fine for me :)
Hope this helps
Edited: I found the link =)
http://wiltonsoftware.com/posts/view/custom-group-by-pagination-and-a-calculated-field
Figured it out:
The paginator counter relies on $this->find('count') to return an integer of the total of the results, which for some reason, doesn't like the 'group' parameter. So following the Custom Query Pagination (which also recommends at the bottom of the page to do the count yourself for any custom / modified pagination) - I added the following to my model:
function paginateCount(){
$params = Configure::read('paginate.params');
$params['fields'] = 'DISTINCT (Job.id)';
unset($params['group']);
unset($params['contain']);
unset($params['order']);
return $this->find('count', $params);
}
This overwrites the value with the correct one and it all seems to be working perfectly.
Bearing in mind I've added Configure::write('paginate', array('params' => $this->paginate['Job'])); to my controller so I can access the pagination parameters.
function paginateCount($conditions = null, $recursive = 0,$extra)
{
$db = $this->getDataSource();
$sql = $db->buildStatement(
array(
'fields' => array('DISTINCT Gdi.id'),
'table' => $db->fullTableName($this),
'alias' => 'Gdi',
'limit' => null,
'offset' => null,
'joins' => isset($extra['joins'])?$extra['joins']:null,
'conditions' => $conditions,
'order' => null,
'group' =>isset($extra['group'])?$extra['group']:null
),
$this
);
$this->recursive = $recursive;
$results = $this->query($sql);
return count($results);
}
Just add this function in your model and change the field name and model name. Using this function you can customize you own query count.

Cakephp DISTINCT

How do I use DISTINCT to get unique user id with highest value for total_time_driven_at_this_trip and also pull user_name from another table which has belongsto relations based on user_id?
I tried this...
$this->set('tripNsws', $this->TripNsw->find('all',array('limit' => 20,'fields' => array('DISTINCT(TripNsw.user_id)','TripNsw.total_time_driven_at_this_trip'),'group' => array('TripNsw.user_id') ,'order' => array('TripNsw.total_time_driven_at_this_trip desc'))));
but it's not working.
I suppose you need to get below....
SELECT DISTINCT(user_id),`total_time_driven_at_this_trip` FROM `trip_nsws` order by `total_time_driven_at_this_trip` desc
// see below url
http://book.cakephp.org/1.3/view/1018/find
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'recursive' => 1, //int
'fields' => array('Model.field1', 'DISTINCT Model.field2'), //array of field names
'order' => array('Model.created', 'Model.field3 DESC'), //string or array defining order
'group' => array('Model.field'), //fields to GROUP BY
'limit' => n, //int
'page' => n, //int
'offset'=>n, //int
'callbacks' => true //other possible values are false, 'before', 'after'
)
// or try this
function some_function() {
$total = $this->Article->find('count');
$pending = $this->Article->find('count', array('conditions' => array('Article.status' => 'pending')));
$authors = $this->Article->User->find('count');
$publishedAuthors = $this->Article->find('count', array(
'fields' => 'DISTINCT Article.user_id',
'conditions' => array('Article.status !=' => 'pending')
));
}
Correct Syntax for DISTINCT keywork in cakephp
$this->set('banners', $this->Banner->find('all',array('fields'=>'DISTINCT Banner.id')));
Make sure DISTINCT uses in fields array.
The correct syntaxis is
$this->set('tripNsws', $this->TripNsw->find('all',array('fields'=>'DISTINCT TripNsw.user_id')));
I've always used a GROUP BY in the query to get the same result as DISTINCT
You can consider setting it as a Virtual Field ( http://book.cakephp.org/1.3/view/1608/Virtual-fields )
$this->Model->virtualFields['distinct_user'] = 'DISTINCT Model.user_id';
'fields' => array('DISTINCT Model.Modelfield')
or
'fields' => array('Model.id','DISTINCT Model.Modelfield')

How can I limit the find to a specific number in CakePHP?

I have a user model which gives me latest users as output. How can I limit the record to just output me 200 records instead of all the users in database?
According to the documentation, the second argument to the find() method is a $params array.
One of the possible values to pass in this array is a limit key. So you could do the following:
$users = $this->User->find('all', array('limit' => 200));
"i have it like array('limit' => 21, 'page' => 1) for paging 21 users in one page.. if i change the limit there to 200 then it paginates 200 users in one page only...in this case how to limit along with proper pagination?? – Anonymous May 14 '09 at 7:22"
yes you can use the cakePHP pagination helper as someone has mentioned. But there may be some cases where you want to do your own pagination or just limit the number of records retrieved per call. For what it's worth here's how I handled one such situation.
Say for example you want to retrieve a certain number of records per page, Then:
$start = 0; -> this is in order to start retrieving records starting from the first one. If you need to say for example, start from the 31st, then $start = 30;
So,
$start = 0;
$length = 20; // we are going to retrieve 20 records starting from the first record
And the code will be something like:
// To retrieve a number of Products per page
$products = $this->Product->find('all', array(
'order' => 'product_number ASC',
'limit' => $start.','.$length,
'recursive' => -1
)
);
Don't paginate with find().
Cake Pagination: http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'recursive' => 1, //int
//array of field names
'fields' => array('Model.field1', 'DISTINCT Model.field2'),
//string or array defining order
'order' => array('Model.created', 'Model.field3 DESC'),
'group' => array('Model.field'), //fields to GROUP BY
'limit' => n, //int
'page' => n, //int
)
Limit * page = 200 set your values according to your comfortable view in pages. This might help
You can also try this out
$results = $this->Model->find('all',
array('limit'=>10,
'order'=>array('date DESC')));
open the model file of user and do as follows:
you will need to change the 'limit' property in the relationship variable named
var $hasMany = array( 'Abus' =>
array('className' => 'Abus',
'foreignKey' => 'user_id',
'dependent' => false, 'conditions'
=> '',
'fields' => '', 'order' => '', 'limit' => '200', 'offset'
=> '', 'exclusive' => '', 'finderQuery' => '',
'counterQuery' => '' ) );
OR you can also try this out...
in your users controller set the $paginate to like this.
var $paginate = array('limit' => 200);
The records will be limited to 200 now wherever you use paginate.

Categories