I am willing to group MySQL results by a column, and also count the records which belong to the same category:
$this->find('all', array(
'fields' => array('UserCheckin.id', 'UserCheckin.capital_id', 'COUNT(UserCheckin.id) as capital_checkins'),
'group' => 'UserCheckin.capital_id'
));
The problem is that capital_checkins is grouped as a separate key in the returned array, whilst I would like it to be under the UserCheckin model. Here's how the results are returned:
Array
(
[0] => Array
(
[UserCheckin] => Array
(
[id] => 3
[capital_id] => 10
)
[0] => Array
(
[capital_checkins] => 2
)
[Capital] => Array
(
[id] => 10
[name] => London
)
)
[1] => Array
(
...
)
)
Is there a way to include the alias under UserCheckin model?
In cakephp 1.3 you can use virtual field:
in your UserCheckin model:
var $virtualFields = array(
'capital_checkins' => 'COUNT(UserCheckin.id)'
);
IMPORTANT UPDATE: As #Rob Wilkerson correctly notices, adding a virtual field with aggregate function permanently to your model will [probably] mess up your find queries which do not have a GROUP BY (implicitly group all rows in MySQL or just fail with SQL error in SQL Server). Solution is to add a virtual field temporarily at a runtime before query that uses it:
$this->virtualFields['capital_checkins'] = 'COUNT(UserCheckin.id)';
$result = $this->find('all', array(
'fields' => array('UserCheckin.id', 'UserCheckin.capital_id', 'UserCheckin.capital_checkins'),
'group' => 'UserCheckin.capital_id'
));
//reset virtual field so it won't mess up subsequent finds
unset($this->virtualFields['capital_checkins']);
return $result;
'COUNT(UserCheckin.id) as UserCheckin__capital_checkins'
http://book.cakephp.org/2.0/en/models/virtual-fields.html#virtual-fields-in-sql-queries
Related
I have two tables posts and users. The post.user_id=users.id.
The fields are
user: id, username, password
post: id, user_id, post_name
I would like to select post.id, post.post_name, user.username.
How to select this in CakePHP?
I have two Models named User and Post. It calls the Model from PostController.
If you have correctly defined the relationship between those two models, this should just happen when you use the Post model's find method:
// In PostsController
$posts = $this->Post->find('all');
// $posts should be an array like this
Array
(
[0] => Array
(
[Post] => Array
(
// Model data
)
[User] => Array
(
// Model data
)
)
)
Hope this helps.
You should properly define the relations between the two models in order to simplify your life, but if you want to use a join, here is the syntax:
find('all', array(
'conditions' => array('name' => 'Thomas Anderson'),
'joins' => array(
array(
'alias' => 'Thought',
'table' => 'thoughts',
'type' => 'LEFT',
'conditions' => '`Thought`.`person_id` = `Person`.`id`'
)
)
));
Imagine I have 2 models: Item and Category. Category hasMany Items and Item belongsTo Category.
I would like to obtain an array "reversed" on how cakephp normally return it: Usually I get something like
[Item]
...some props...
[Category]
...some props...
While I want something like:
Array
[0][Category]
Array
[0][Item]
...some props...
[1][Item]
[1][Category]
...and so on
How can I obtain this result with find method if possible? Otherwise, how to obtain it?
My current model is much more complex but I think I could apply this teory to all my models (I have a Category => Group => Item relationship).
Thanks for suggestions and answers.
According to the title of your question, if the point is to get only Items that are linked to categories named 'bla', you can just call the find() method on the Category model instead of the Item model.
Instead of
$this->Item->find('all', array('conditions' => array('Category.name' => 'bla')));
do
$this->Item->Category->find('all', array('conditions' => array('Category.name' => 'bla')));
This way it will first filter the corresponding Categories and then get the linked Items. So you will obtain something like
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 1
[name] => bla
...
)
[Item] => Array
(
[0] => Array
(
[id] => 1
...
)
[1] => Array
(
[id] => 2
...
)
)
)
)
EDIT
With three models linked together, it is a bit more complicated. The only solution I can think of to prevent loading a lot of data is to build a query with INNER JOINs:
$categories = $this->Category->find('all', array('fields' => array('Category.id', 'Category.name', 'Item.id', 'Item.name'),
'joins' => array(
array( 'table' => 'groups',
'alias' => 'Group',
'type' => 'inner',
'conditions' => 'Group.category_id = Category.id'
),
array( 'table' => 'items',
'alias' => 'Item',
'type' => 'inner',
'conditions' => array('Item.group_id = Group.id', 'Item.name' => 'bla')
))));
It will return an array like this:
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 2
[name] => Shop
)
[Item] => Array
(
[id] => 1
[name] => Bla
)
)
)
EDIT2
A last word about the structure of the array you'll get: it is not exactly like you wanted. The Item array is at the same level that the Category array. So you may end up with many pairs of Category-Item for the same Category.
But a simple foreach loop would then allow you to build an array with the exact struture you wish and this query has the advantage to get only what is neccessary from the database.
I am trying to use CakePHP 1.3.5's searchable behavior with containable behavior to return search results for a specified model and an associated model (Article belongsTo User).
Ignoring the searchable behavior for a moment, the following call to find():
$this->Article->find('all', array(
'conditions' => array('Article.is_published' => 1),
'fields' => array('Article.id'),
'contain' => array('User.name')
));
Executes this SQL query:
SELECT `Article`.`id`, `User`.`name`, `User`.`id` FROM `articles` AS `Article` LEFT JOIN `users` AS `User` ON (`Article`.`user_id` = `User`.`id`) WHERE `Article`.`is_published` = 1
And returns the following array:
Array (
[0] => Array (
[Article] => Array (
[id] => 10
)
[User] => Array (
[name] => Author Name
[id] => 7
)
)
...
)
Which is exactly what's expected. However, the following call to search():
$this->Article->search($query, array(
'conditions' => array('Article.is_published' => 1),
'fields' => array('Article.id'),
'contain' => array('Article' => array('User.name'))
));
Executes this SQL query:
SELECT `Article`.`id` FROM `search_index` AS `SearchIndex` LEFT JOIN `articles` AS `Article` ON (`SearchIndex`.`model` = 'Article' AND `SearchIndex`.`association_key` = `Article`.`id`) WHERE `Article`.`is_published` = 1 AND MATCH(`SearchIndex`.`data`) AGAINST('search term' IN BOOLEAN MODE) AND `Article`.`id` IS NOT NULL
And returns this array:
Array (
[0] => Array (
[Article] => Array (
[id] => 9
)
)
...
)
Looking at the search() method, it is returning $this->SearchIndex->find('all', $findOptions);. $findOptions contains the following:
Array (
[conditions] => Array (
[Article.is_published] => 1
[0] => MATCH(SearchIndex.data) AGAINST('search term' IN BOOLEAN MODE)
)
[fields] => Array (
[0] => Article.id
)
[contain] => Array (
[Article] => Array (
[0] => User.name
)
)
)
The association isn't getting lost along the way, because inside SearchableBehavior, $this->SearchIndex->Article->belongsTo['User'] is present and intact immediately before and after the call to find() inside the search() method.
The call to search() returns the exact same thing for all of the following values of 'contain':
array('Article' => array('User.name'))
array('Article' => array('User'))
array('Article' => array('User' => array()))
array('Article' => array('User' => array('fields' => array('User.name'))))
array('Article' => array('User' => array('fields' => array('name'))))
Am I doing something wrong? I think I'm using the same format as is instructed in the CakePHP documentation, and I haven't found anything online that suggests that you have to do something special to get search results with associated data.
I know that I could easily achieve the result that I want by just looking up the Users with additional calls to find(), but I'd like to get containable behavior to work like it's supposed to and cut down on unnecessary extra database queries.
When using containable, set the recursive option to "true"
$this->Model->Behaviors->attach("Containable",array("recursive"=>true));
I have been busy with the cakePHP framework for a couple of months now and I really love it. At the moment I'm working on a very new project and it does the job like it should (I think ...) but I feel uncomfortable with some code I wrote. In fact I should optimize my paginate conditions query so I get immediately the right results (right now I manipulate the result set by a bunch of Set::extract method calls.
I'll sketch the relevant aspects of the application. I have a model 'Site' who has a hasMany relationship with the model 'SiteMeta'. This last table looks as follow: id, site_id, key, value, created.
In this last model I record several values of the site at various periods. The name of the key I want to store (e.g. alexarank, google pagerank, ...), and off course also the value. At a given interval I let my app update this database so I can track evolution of this values.
Now my problem is this.
On the overview page of the various websites (controller => Sites, action => index) I'd like to show the CURRENT pagerank of the website. Thus I need one exact SiteMeta record where the 'created' field is the highest and the value in 'key' should be matching the word 'pagerank'. I've tried several things I read on the net but got none of them working (containable, bindmodel, etc.). Probably I'm doing something wrong.
Right now I get results like this when I do a $this->paginate
Array
(
[0] => Array
(
[Site] => Array
(
[id] => 1
[parent_id] => 0
[title] => test
[url] => http://www.test.com
[slug] => www_test_com
[keywords] => cpc,seo
[language_id] => 1
)
[SiteMeta] => Array
(
[0] => Array
(
[id] => 1
[site_id] => 1
[key] => pagerank
[value] => 5
[created] => 2010-08-03 00:00:00
)
[1] => Array
(
[id] => 2
[site_id] => 1
[key] => pagerank
[value] => 2
[created] => 2010-08-17 00:00:00
)
[2] => Array
(
[id] => 5
[site_id] => 1
[key] => alexa
[value] => 1900000
[created] => 2010-08-10 17:39:06
)
)
)
To get the pagerank I just loop through all the sites and manipulate this array I get. Next I filter the results with Set::extract. But this doens't feel quite right :)
$sitesToCheck = $this->paginate($this->_searchConditions($this->params));
foreach($sitesToCheck as $site) {
$pagerank = $this->_getPageRank($site['Site']);
$alexa = $this->_getAlexa($site['Site']);
$site['Site']['pagerank'] = $pagerank;
$sites[] = $site;
}
if (isset($this->params['named']['gpr']) && $this->params['named']['gpr']) {
$rank = explode('-', $this->params['named']['gpr']);
$min = $rank[0];$max = $rank[1];
$sites = Set::extract('/Site[pagerank<=' . $max . '][pagerank>=' . $min .']', $sites);
}
$this->set(compact('sites', 'direction'));
Could you guys please help me to think about a solution for this? Thanks in advance.
Thanks for the contributions. I tried these options (also something with bindmodel but not working also) but still can't get this to work like it should be. If I define this
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' =>array('Site.id = SiteMeta.site_id')
)
),
);
I get duplicate results
I have a site with 3 different SiteMeta records and a site with 2 different record.
The paginate method returns me 5 records in total. There's probably an easy solution for this, but I can't figure it out :)
Also I tried to write a sql query myself, but seems I can't use the pagination magic in that case. Query I'd like to imitate with pagination options and conditions is the following. The query returns exactly as I would like to get.
$sites = $this->Site->query('SELECT * FROM sites Site, site_metas SiteMeta WHERE SiteMeta.id = (select SiteMeta.id from site_metas SiteMeta WHERE Site.id = SiteMeta.site_id AND SiteMeta.key = \'pagerank\' order by created desc limit 0,1 )');
As you are trying to retrieve data in a hasMany relationship, cakephp doesn't join the tables by default. If you go for joins you can do something like:
$this->paginate = array(
'joins'=>array(
array(
'table'=>'accounts',
'alias'=>'Account',
'type' =>'inner',
'conditions' =>array('User.id = Account.user_id')
)
),
'conditions'=> array('OR' =>
array(
'Account.name'=>$this->params['named']['nickname'],
'User.id' => 5)
)
);
$users = $this->paginate();
$this->set('users',$users);
debug($users);
$this->render('/users/index');
You have to fit this according to your needs of course. More on joins, like already mentioned in another answer.
Edit 1: This is because you are missing the second 'conditions'. See my code snippet. The first 'conditions' just states where the join happens, whereas the second 'conditions' makes the actual selection.
Edit 2: Here some info on how to write conditions in order to select needed data. You may want to use the max function of your rdbms on column created in your refined condition.
Edit 3: Containable and joins should not be used together. Quoted from the manual: Using joins with Containable behavior could lead to some SQL errors (duplicate tables), so you need to use the joins method as an alternative for Containable if your main goal is to perform searches based on related data. Containable is best suited to restricting the amount of related data brought by a find statement. You have not tried my edit 2 yet, I think.
Edit 4: One possible solution could be to add a field last_updated to the table Sites. This field can then be used in the second conditions statement to compare with the SiteMeta.created value.
Try something like this:
$this->paginate = array(
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->paginate('Site');
Or this:
$conditions = array(
'recursive' => 1,
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->Site->find('all', $conditions);
If that does not work check this and this. I am 100% sure that it is possible to get the result you want with a single query.
Try something like this (with containable set up on your models):
$this->Site->recursive = -1;
$this->paginate = array(
'conditions' => array(
'Site.title' => 'title') //or whatever conditions you want... if any
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => 'SiteMeta.created DESC')));
I use containable so much that I actually have this in my app_model file so it applies to all models:
var $actsAs = array('Containable');
Many thinks to all who managed to help me through this :)
I got it fixed after all hehe.
Eventually this has been the trick for me
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' => array('Site.id = SiteMeta.site_id'))
),
'group' => 'Site.id',
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => SiteMeta.created DESC',
)));
$sites = $this->paginate();
Cake handles pagination of a model with a simple $this->paginate(), but what should I use if I want to paginate a array of values?
The Scenario is like this:
$this->set('sitepages', $this->paginate());
This code in my index() returns an array like
Array
(
[0] => Array
(
[Sitepage] => Array
(
[id] => 13
[name] => Home
[urlslug] => home
[parent_id] => 1
[page_title] => Welcome to KIAMS, Pune
[order] => 1
)
)
[1] => Array
(
[Sitepage] => Array
(
[id] => 26
[name] => About Us
[urlslug] => aboutus
[parent_id] => 1
[page_title] =>
[order] => 2
)
)
[2] => Array
(
[Sitepage] => Array
(
[id] => 27
[name] => Overview of KIAMS
[urlslug] => aboutus/overview
[parent_id] => 26
[page_title] =>
[order] => 2
)
)
I retrieved the same data using $this->Sitepage->find('all') and then performed some manipulations as required and form a array which is very similar to the above one, but the ordering gets changed. I want to paginate this new array and pass it to the view. I tried
$this->set('sitepages',$this->paginate($newarray))
But the data is not getting paginated. Can some one please help with paginating the $newarray in CakePHP?
To paginate in CakePHP you need to pass select conditions to the paginate() call.
Other data manipulation should be done in afterFind(), in your model file.
If you don't need these changes to be done in every single retrieval, you might as well consider creating a new model file pointing to the very same table as the current one, and adding an afterFind() method to that new file.
I've just dealt with this same problem...
I found the only way is to use the paginate() function to handle all the logic, rather than passing it a custom array. However, this isn't as bad as it seems:
$this->paginate = array(
'limit' => 2,
'order' => array(
'Report.name' => 'asc'
),
'conditions' => array(
'Account.id' => $this->foo()
),
);
$reports = $this->paginate();
In this example, I'm paginating Reports - but some Reports will not be included, depending on which Account they belong to (Account has some relationship with Report, hasmany, etc.).
By writing $paginate inside your action, you can use a custom callback for the conditions array. So function foo() can be written in your controller (or shoved to model) and returns an array of valid Account IDs.
I found I could easily rewrite my custom logic in this function - keeping both me and the Cake paginator happy.
Hope that helps!
I'm using cakephp version 1.3 and it seems this one is working:
at controller:
$result = $this->Paginate('Event');
$results = $this->Task->humanizeEvent($result);
$this->set('events', $results);
and it seems to display as a normal paginated array, at the view (setup your pagination in view as normal).
The humanizeEvent function just edits a field on the results array, to make it a sentence based on other fields inside the array, and it seems to work properly.
$this->paginate($newarray) is the wrong way to paginate. The first parameter cannot be an array. It must be a model name. You may want to study pagination setup from the manual. This will order alphabetically:
var $paginate = array(
'limit' => 25,
'order' => array(
'Sitepage.name' => 'asc'
)
);
$this->set('sitepages', $this->paginate('Sitepage'));
I create a component checking the paginator code..
Is not the best thing, but It work for me.....
Controller
$slicedArray = array_slice($fullArray,($page - 1) * $this->PaginatorArray->limit ,$this->PaginatorArray->limit)
$this->params['paging'] = $this->PaginatorArray->getParamsPaging('MyModel', $page, $total,count($slicedArray));
$this->helpers[] = 'Paginator';
Component
<?php
/* SVN FILE: $Id$ */
/**
* Pagination Array Component class file.
* #subpackage cake.cake.libs.view.helpers
*/
class PaginatorArrayComponent {
var $limit = 40;
var $step = 1;
function startup( & $controller){
$this->controller = & $controller;
}
function getParamsPaging($model, $page, $total, $current){
$pageCount = ceil($total / $this->limit);
$prevPage = '';
$nextPage = '';
if($page > 1)
$prevPage = $page - 1;
if($page + 1 <= $pageCount)
$nextPage = $page + 1;
return array(
$model => array(
'page' => $page,
'current' => $current,
'count' => $total,
'prevPage' => $prevPage,
'nextPage' => $nextPage,
'pageCount' => $pageCount,
'defaults' => array(
'limit' => $this->limit,
'step' => $this->step,
'order' => array(),
'conditions' => array(),
),
'options' => array(
'page' => $page,
'limit' => $this->limit,
'order' => array(),
'conditions' => array(),
)
)
);
}
}
?>
There was a bug in find("count") and it returned incorrect count if the query resulted in records for only in group. This has been fixed click here