I have a paginator as follows:
var $paginate = array(
'order'=>array('ReleaseServer.server_environment'=>'ASC',
'ReleaseServer.server_name'=>'ASC'),
'joins'=>array(
array(
'table' => 'release_server_to_components',
'alias' => 'ReleaseServerToComponent',
'type' => 'LEFT',
'foreignKey' => false,
'conditions'=> array('ReleaseServer.id = ReleaseServerToComponent.release_server_id')
),
array(
'table' => 'release_components',
'alias' => 'ReleaseComponent',
'type' => 'LEFT',
'foreignKey' => false,
'conditions'=> array('ReleaseServerToComponent.release_component_id = ReleaseComponent.id')
)
),
'group'=>array('ReleaseServer.id'),
'contain' => array(
'ReleaseServerToComponent' => array(
'ReleaseComponent' => array(
'Release'
)
)
),
'limit' => 25,
);
Then in my controller function I do the following:
$this->set('allServers', $this->paginate('ReleaseServer', $conditions));
Where $conditions are some extra conditions for the query.
As you see above I set the limit at 25.
There are 29 records in the database however, but the page only shows 25 and the page says there is only one page.
but when a person clicks on one of the column headers to order them, some rows that were not there before magically appear, and others disappear. Why would this be?
If you need any other info please let me know
UPDATE
Now i see that the problem resides in the group part of the paginate variable, but i need it in order to make it so I do not get multiple rows of the same thing.
How do I fix that?
I solved it by adding a new paginateCount() function to my model:
function paginateCount($conditions = null, $recursive = 0, $extra = array())
{
$count = $this->find('count', array(
'fields' => 'DISTINCT ReleaseServer.id',
'conditions' => $conditions
));
return $count;
}
Related
I'm working inside a legacy Cake PHP 2.10 application and am utilising both the Pagination component and PaginationHelper.
I've had to override the paginateCount method to set a custom option for my paginator settings to limit the maximum number of query results which are then paginated, limit and maxLimit are not sufficient on their own - I've added totalLimit.
The problem now is if there are less results than my totalLimit, such as when filtering, it continues to display 20,000 rather than the number of actual results. I could create another query specifically for this count, but wondered if there's a quick workaround that I'm missing, here's my my Application model method:
public function paginateCount($conditions = null, $recursive = 0, $extra = array())
{
if (isset($extra['totalLimit'])) {
return $extra['totalLimit'];
}
}
And my pagination set up:
// define pagination settings
$this->Paginator->settings = array(
'Application' => array(
'paramType' => 'querystring',
'totalLimit' => 20000,
'limit' => $filters['pagination']['perPage'],
'maxLimit' => $filters['pagination']['perPage'],
'fields' => array(
'Application.*',
'ApplicationPayday.*',
'ApplicationApiLink.*',
'ApplicationResponse.*',
'AffiliateId.*',
'Redirect.*'
),
'joins' => array(
array(
'table' => 'tlp_application_paydays',
'alias' => 'ApplicationPayday',
'type' => 'LEFT',
'conditions' => array(
'ApplicationPayday.application_id = Application.id'
)
),
array(
'table' => 'tlp_application_api_links',
'alias' => 'ApplicationApiLink',
'type' => 'LEFT',
'conditions' => array(
'ApplicationApiLink.application_id = Application.id'
)
),
array(
'table' => 'tlp_application_responses',
'alias' => 'ApplicationResponse',
'type' => 'LEFT',
'conditions' => array(
'ApplicationResponse.application_id = Application.id'
)
),
array(
'table' => 'tlp_affiliate_ids',
'alias' => 'AffiliateId',
'type' => 'LEFT',
'conditions' => array(
'AffiliateId.aff_id = Application.tlp_aff_id'
)
),
array(
'table' => 'tlp_redirects',
'alias' => 'Redirect',
'type' => 'LEFT',
'conditions' => array(
'Redirect.application_id = Application.id'
)
)
),
'conditions' => $queryConditions,
'group' => array(
'Application.id'
),
'order' => array(
'Application.id' => 'desc'
),
'recursive' => -1
)
);
// run query to get applications via paginated settings
try {
$applications = $this->Paginator->paginate('Application');
} catch (\NotFoundException $e) {
$this->Session->setFlash("Page doesn't exist. We've reset your search filters and taken you to the first page.");
return $this->redirect('/payday_admin/leads/');
}
The problem is that you replace the number of total results that paginateCounts returns with $extra['totalLimit'] that you set (20.000 in this case).
The pagineCount() function should be overridden with something like in the example below. This way you will insert your total limit in the ->find('count', []) parameters and also keep the original count if your $extra['totalLimit'] parameter is not sent.
public function paginateCount($conditions, $recursive, $extra)
{
if (isset($extra['totalLimit'])) {
$limit = $extra['totalLimit'];
unset($extra['totalLimit']);
$count = $this->find('count', compact($conditions, $recursive, $limit, $extra));
return (int)$count;
}
$count = $this->find('count', compact($conditions, $recursive, $extra));
return (int)$count;
}
Your count will be limited to the maximum value of totalLimit, but will return the true count if it's lower.
If you have millions of rows with many joins I recommend caching the count. The cache key can be created by hashing the conditions and other parameters.
This is how the conditions are passed to the overridden paginateCount() from the model Class.
$this->Paginator->settings = [
'limit' => 50,
'contain' => [
...
],
'conditions' => $conditions,
'joins' => $joins,
'group' => $group
];
$this->Paginator->paginate('Model');
My requirement is to search on 2 different tables : Vets and Clinics. There might be relation between them i.e result should fetch clinics have 'a' in name and vets having 'a' in them. Vets might be related to clinics or might not. Currently I'm doing the following. Is there any method to avoid running 2 queries which can also help me use the cakephp pagination helper?
$this->paginate = array(
'Vet' => array(
'conditions' => $conditions,
'fields' => array('Vet.id', 'Vet.name', 'Vet.professionnal_address', 'phone_number', 'Vet.email', 'Vet.type', 'Vet.latitude', 'Vet.longitude','Vet.city','Vet.clinic_id','Vet.zipcode'),
'joins' => array(
array(
'table' => 'vet_appointment_types',
'alias' => 'VetAppointmentType',
'type' => 'LEFT',
'conditions' => array(
'Vet.id = VetAppointmentType.vet_id',
)
)
),
'limit' => $limit,
'group' => array(
'Vet.id'
),
'order' => array(
'Vet.name' => 'ASC'
)
),
'Clinic' => array(
'conditions' => $conditions1,
'fields' => array('Clinic.*'),
'limit' => $limit,
'order' => array(
'Clinic.name' => 'ASC'
)
)
);
$results = $this->paginate('Vet');
$results2 = $this->paginate('Clinic');
I tried by extending the default pagination component in a non database model and using union but the database structure in a bit complex so can't use union. Also, i think implementing a temporary table based model would be an option, but since it'll be used for searching, so how exactly to go about implementing it, I'm unable to think. Any help would be +1'd ;)
As per the suggestion, I accomplished it by using database views. Below is the code. Any improvement suggestions is highly appreciated.
// \app\Model\Search.php
App::uses('ConnectionManager', 'Model');
App::uses('AppModel', 'Model');
App::uses('CakeLog', 'Log');
class Search extends AppModel {
public function __construct() {
parent::__construct();
$connection = ConnectionManager::getDataSource('default');
$return = $connection->execute("CREATE OR REPLACE VIEW `search` AS
SELECT Vet.id, (CONCAT( `Vet`.`fname` , ' ', `Vet`.`lname` )) AS name, 'vet' AS TYPE , Vet.latitude, Vet.longitude, Vet.zipcode, Vet.speciality FROM `db`.`vets` AS `Vet`
UNION
SELECT Clinic.id, Clinic.name, 'clinic' AS TYPE , Clinic.lat, Clinic.long, Clinic.zipcode, Clinic.address FROM `db`.`clinics` AS `Clinic`");
$return = ($return == true)?'Search View created successfully on '.date('Y-m-d H:i:s'):$return;
CakeLog::write('search', $return);
}
public $useTable = 'search';// This model does not use a database table
public $primaryKey = 'id'; // Define primary key
public $useDbConfig = 'default'; // Define db
}
This is the model.Everytime user searches anything, when the model is loaded in controller, view is created/replaced so that updated values can be fetched. It can be used as any other model in cakephp. It also supports virtualFields as well. To build your query for the database view you can also use the query builder which i used as follows.
$joins = array(
array(
'table' => 'vet_appointment_types',
'alias' => 'VetAppointmentType',
'type' => 'LEFT',
'conditions' => array(
'Vet.id = VetAppointmentType.vet_id',
)
)
);
$dbo = $this->Vet->getDataSource();
$subQuery = $dbo->buildStatement(
array(
'fields' => array('Vet.id', 'Vet.name'),
'table' => $dbo->fullTableName($this->Vet),
'alias' => 'Vet',
'limit' => $limit,
'group' => array(
'Vet.id'
),
'order' => array(
'Vet.name' => 'ASC'
),
'offset' => null,
'joins' => $joins,
'conditions' => $conditions
),
$this->Vet
);
$query = $subQuery;
$query .= ' UNION ';
$dbo = $this->Clinic->getDataSource();
$subQuery = $dbo->buildStatement(
array(
'fields' => array('Clinic.id', 'Clinic.name'),
'table' => $dbo->fullTableName($this->Clinic),
'alias' => 'Clinic',
'limit' => $limit,
'conditions' => $conditions1,
'limit' => $limit,
'order' => array(
'Clinic.name' => 'ASC'
),
'offset' => null
),
$this->Clinic
);
$query .= $subQuery;
print_r($query);
I have an Item model which has the following associations:
public $hasOne = array(
'Project' => array(
'className' => 'Project',
'foreignKey' => 'item_id'
)
);
public $hasMany = array(
'ItemPic' => array(
'className' => 'ItemPic',
'foreignKey' => 'item_id',
'dependent' => false
)
);
I am wanting custom data for different views of Item. It seems like CakePHP automatically includes Project data (maybe because it is hasOne?) and does not include the ItemPic data. In the index I really don't even want the Project data... however, I do want the ItemPic data. For each Item record pulled, I want a single ItemPic record joined to it. This ItemPic should be basically ItemPic.item_id = Item.id and ORDER BY ItemPic.rank LIMIT 1.
The purpose of this is basically so that in the index I can show a list of Items and a picture associated with each item. I would like all of the images along with the Project data in the view for a single Item, but not in the list/index.
I was told I could use containable like this:
// In the model
public $actsAs = array('Containable');
// In the controller
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
);
The above actually works how I want... however, I was also told that doing this would cause an extra query to be ran for every single Item... which I feel I should avoid.
I tried doing this, but I get duplicate data and it doesn't attach any ItemPic data:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'table' => 'item_pics',
'alias' => 'ItemPic',
'type' => 'LEFT',
'conditions' => array(
'ItemPic.item_id = Item.id'
),
'order' => 'rank ASC',
'limit' => 1
)
)
);
$paginated = $this->Paginator->paginate();
Can you please try this:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'table' => 'item_pics',
'alias' => 'ItemPic',
'type' => 'LEFT',
'conditions' => array(
'ItemPic.item_id = Item.id'
),
'order' => 'rank ASC',
'limit' => 1
)
),
'fields' => array('Item.*','Project.*','ItemPic.*')
);
In the fileds section you may or may not assign "Item" , "Project" according to your requirment.
Thanks
I am using CakePHP 2.0 and I have a Model with this 'virtualFields':
Country.php:
var $virtualFields = array(
'path' => "CONCAT_WS('/', dirname, basename)"
);
If I do this in a Controller which uses "User"
$users = $this->User->find('all');
the virtual field path is set.
If I use this in my Controller, which also uses "User"
$options['fields'] = array(
'DISTINCT User.*'
);
$options['joins'] = array(
array(
'table' => 'courses',
'type' => 'inner',
'conditions' => array(
'User.id = courses.user_id'
)
),
array(
'table' => 'times',
'type' => 'inner',
'conditions' => array(
'courses.id = times.course_id'
)
)
);
$options['conditions'] = array(
'times.amount > ' => 0
);
$users = $this->User->find('all', $options);
With the options, the path field is not set, of course, the SQL query does not seem to have the "CONCAT_WS('/', dirname, basename)" field included, which is included if I do the find operation without the options.
What can I do having the options, so that the virtual field is automatically included? Of course I can write in the fields options the CONCAT, but that's not very nice, especially if I change it.
Best regards.
Use group by rather than distinct and remove the fields option completely.
$options['group'] = array(
'User.*'
);
$options['joins'] = array(
array(
'table' => 'courses',
'type' => 'inner',
'conditions' => array(
'User.id = courses.user_id'
)
),
array(
'table' => 'times',
'type' => 'inner',
'conditions' => array(
'courses.id = times.course_id'
)
)
);
$options['conditions'] = array(
'times.amount > ' => 0
);
$users = $this->User->find('all', $options);
In Cakephp is there a better way to write this:
$unread_orders = $this->Order->find('all', array('conditions' => array('Order.status' => 'unread') ));
$read_orders = $this->Order->find('all', array('conditions' => array('Order.status' => 'read') ));
$dispatched = $this->Order->find('all', array('conditions' => array('Order.status' => 'dispatched'), 'limit' => 5));
$canceled = $this->Order->find('all', array('conditions' => array('Order.status' => 'canceled'), 'limit' => 5));
There is a limit on the dispatched and canceled items.
It's seems like there would be a more effcient way of doing this, in one call to the database instead of 4.
Cheers.
One way is to do
$orders_read_unread = $this->Order->find('all', array('conditions' => array('OR' => array(array('Order.status' => 'unread'), array('Order.status' => 'read')))));
$orders_disp_cancel = $this->Order->find('all', array('conditions' => array('OR' => array(array('Order.status' => 'canceled'), array('Order.status' => 'dispatched'))), 'limit' => 5));
EDIT: Updated queries. Thanks Mark for clarifying.
<?php
...
$orders_read_unread = $this->Order->find( 'all', array(
'conditions' => array( 'Order.status' => array( 'unread', 'read' )),
'group' => array( 'Order.status' ),
));
/**
* Use this if you need 5 of EITHER canceled OR dispatched order
* if you need 5 of EACH you need to break it into two queries.
*/
$orders_dispatched_canceled = $this->Order->find( 'all', array(
'conditions' => array( 'Order.status' => array( 'canceled', 'dispatched' )),
'group' => array( 'Order.status' ),
'limit' => 5
));
/**
* Use these if you need 5 of EACH dispatched AND canceled orders
*/
$orders_dispatched = $this->Order->find( 'all', array(
'conditions' => array( 'Order.status' => 'dispatched' ),
'group' => array( 'Order.status' ),
'limit' => 5
));
$orders_canceled = $this->Order->find( 'all', array(
'conditions' => array( 'Order.status' => 'canceled' ),
'group' => array( 'Order.status' ),
'limit' => 5
));
...
?>
Should do the trick for you without having to deal with the 'OR' key syntax. It will generate a slightly less efficient IN ARRAY( '..' ,'..' ) syntax but keeps the php a little cleaner.
As an alternative you could look at either sub-queries - which are a pain with Cake. The book has an example of using the query builder via the datasource to inject a query into the conditions array of a normal cake find call.
http://book.cakephp.org/view/1030/Complex-Find-Conditions
And remember both of these finds should be in the model inside a function - you can either define a custom find type or just call a model function directly from your controller.
http://www.pixelastic.com/blog/88:using-custom-find-and-paginate-methods-in-cakephp-1-3