Using the Lithium framework, I have two model classes, Post and Image. They are related via a hasMany relationship.
//Post.php
class Post extends \lithium\data\Model {
public $hasMany = array('Image');
}
In one of my queries in my PostsController, I am attempting to find a Post with Images that are sorted by an order and have a limit. However the order and limit options on the Images are being ignored. Here is what my query looks like:
//PostsController.php
$post = Post::find('first', array(
'with'=>array(
'Image'=>array(
'order'=>array('position'=>'ASC'),
'limit'=>3
)
),
'conditions'=>array(
'Post.id'=>'some-id-value'
)
));
This particular query is returning the post with ALL of its related images, not sorted by 'position.' For example if this post had 10 images related to it, all 10 images are being returned with it instead of just the limit of 3, sorted by position.
In general, the idea is that I want to be able to order and limit a model's related hasMany data. Is this possible with the hasMany relationship in Lithium or will hasMany always return all the related data no matter what? Clearly what I am attempting is not the correct way of doing it.
Here's how you can use ORDER BY with Lithium Relationships ...
$table1_table2 = Table1::find('all', array(
'conditions' => array('Table1.foo' => 'bar'),
'with' => array('Table2'),
'order' => array('Table2.title'),
'limit' => 3
));
Note the 'Table2.title' which wll result in something like ...
SELECT ... ORDER BY Table2.title;
The ASC is implied in this case, but you can pass ASC or DESC as well.
Do you can add a final query wich do you need? As I know, MySQL can't limit and order in JOINs at all, only in subqueries.
So, in this case, 2 requests is better than 1 join:
first Post::find(...conditions...) then
$images = Image::find(array(
'order' => array('post_id','position'),
'limit' => 3,
'conditions' => array(
'post_id' => $array_of_post_id_from_first_query
)
);
Related
I have 2 tables joined by a HABTM relationship. portfolios and assets. The join table is portfolios_assets.
I wish to have an extra field in the join table, rebalance_date such that I can query the assets in a portfolio for a given date. How would I construct a find such that I can determine the most recent date and only return the assets for that date.
So, in my Portfolio model I might have:
$params = array(
'conditions' => array(
'Portfolio.id' => 5,
'?.rebalance_date' => '2013-11-01' //no model name for this field as it's in the join table
),
'order' => array(...)
);
$result = $this->find('all', $params);
In the above example, I just keyed in a date. I'm not sure how I would retrieve the latest date without writing a raw query in Cake. (I could do SELECT rebalance_date FROM portfolios_assets ORDER BY rebalance_date DESC LIMIT 1; but this is not following Cake's convention)
You need to use the hasMany through model: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany-through-the-join-model
You would need to create another model eg PortfolioAssests and the table would need to be portfolio_assets not portfolios_assets.
Then you should be able to use:
$assets = $this->Assets->find('all', array(
'conditions' => array(
'Portfolio.id' => 5,
'PortfolioAsset.rebalance_date' => '2013-11-01'
)
));
I've been quite some time trying to use the Containable Behavior in CakePHP but I can't get to make it work as I expected.
My application is different, but to simplify I'll put this example. Let's say I have a forum with threads and activities, and the activities can be rated. The general relations would be:
Forum: hasMany [Thread]
Thread: belongsTo [Forum], hasMany [Activity]
Activity: belongsTo [Thread], hasMany [Rating]
Rating: belongsTo [Activity]
What I want to achieve is, using the find method, get all the ratings performed on a certain forum. What I suppose should be done is the following:
$this->Rating->find('count', array(
'contain' => array(
'Activity' => array(
'Thread'
)
),
'conditions' => array(
'Thread.forum_id' => 1
)
));
But the result query is:
SELECT COUNT(*) AS `count` FROM `ratings` AS `Rating` LEFT JOIN `activities` AS `Activity` ON (`Rating`.`activity_id` = `Activity`.`id`) WHERE `Thread`.`forum_id` = 1;
I've accomplished this using the 'joins' option, but it's more complex and I have to use this kinda action in many situations.
All the files related with the example can be found here: http://dl.dropbox.com/u/3285746/StackOverflow-ContainableBehavior.rar
Thanks
Update 23/11/2011
After investigating the framework and thanks to the answers of Moz Morris and api55 I found the source of the problem.
The basic problem was that, as I understood CakePHP, I thought it was querying using joins each time. The thing it that it doesn't do that, the real operation it would perform to obtain the result I was looking for would be something like this:
SELECT * FROM Rating JOIN Activity...
SELECT * FROM Activity JOIN Thread...
SELECT * FROM Activity JOIN Thread...
...
Meaning that it would do a query to get all the activities and then, for each activity, perform a query to get the Threads... My approach was failing not because of the Containable Behaviour being used wrong, but because the 'conditions' option was applied to all queries and, on the first one, it crashed because of the absence of the Thread table. After finding this out, there are two possible solutions:
As api55 said, using the conditions inside the 'contain' array it would apply them only to the queries using the Thread table. But doing this the problem persists, because we have way too many queries.
As Moz Morris said, binding the Thread model to Rating would also work, and it would perform a single query, which is what we want. The problem is that I see that as a patch that skips the relations betweem models and doesn't follow CakePHP philosophy.
I marked api55 solution as the correct because It solves the concrete problem I had, but both give a solution to the problem.
First of all, have you put the actAs containable variable in the appModel?? without it this beahaviour won't work at all (i see it is not working correctly since it didn't join with Thread table)
I would do it from the top, i mean from forum, so you choose your forum (im not sure you want forum or thread) and get all its rating, if theres no rating you will end up with the rating key empty.
something like this
appModel
public $actsAs = array('Containable');
rating controller
$this->Rating->Activity->Thread->Forum->find('count', array(
'contain' => array(
'Thread' => array(
'Activity' => array(
'Rating' => array (
'fields' => array ( 'Rating.*' )
)
)
)
),
'conditions' => array(
'Forum.id' => 1
)
));
Then if you need only a value in rating table just use Set:extract to get an array of this value.
As you did it IT SHOULD work anyways but i sugest not to use forum_id there, but in conditions inside contain like this
'contain' => array(
'Activity' => array(
'Thread' => array(
'conditions' => array('Thread.forum_id' => 1)
)
)
),
Also, never forget the actsAs variable in the model using the containable behaviuor (or in app model)
Whist I like api55's solution, I think the results are a little messy - depends on what you intend to do with the data I guess.
I assume that when you said using the 'joins' method you were talking about using this method:
$this->Rating->bindModel(array(
'belongsTo' => array(
'Thread' => array(
'foreignKey' => false,
'conditions' => 'Thread.id = Activity.thread_id',
),
'Forum' => array(
'foreignKey' => false,
'conditions' => 'Forum.id = Thread.forum_id'
)
)
));
$ratings = $this->Rating->find('all', array(
'conditions' => array(
'Forum.id' => 1 // insert forum id here
)
));
This just seems a little cleaner to me, and you don't have to worry about using the containable behaviour in your AppModel. Worth considering.
I am trying to force a join on paginate function of cakephp.
A user has messages which means it will be message belongs to user.
I have to show them in a list so i need to use paginate here.
Problem is it doesnt shows me the record of the model which i intend to bind
My code is:
$userId = $this->Session->read('SESSION_ADMIN.id');
$this->helpers['Paginator'] = array('ajax' => 'Ajax');
$this->Message->bindModel(
array(
'belongsTo'=>array(
'Npo'=>array(
'className' => 'Npo',
'foreignKey' => 'reciever_id',
'fields' => 'Npo.username'
)
)
)
);
$this->paginate = array('conditions'=>array('Message.sender_id'=>$userId,'Message.sender'=>'Admin'),
'order' => array('Message.modified DESC'),
'limit' =>'1'
);
$sentMsg = $this->paginate('Message');
//$sentMsg = $this->Message->find('all');
pr($sentMsg);die();
when i uncomment the FIND statement it shows me record but in case of paginate it doesnt.
Also it doesnt shows me the join in the paginate query but it does in counting record.
Any one have an idea.I dont want to use paginate Join here.Is there a way to enforce a belongs to here?
Regards
Himanshu Sharma
Have you tried:
$this->Message->bindModel(
array(
'belongsTo'=>array(
'Npo'=>array(
'className' => 'Npo',
'foreignKey' => 'reciever_id',
'fields' => 'Npo.username'
)
)
), false // Note the false here!
);
The paginator actually executes two queries: one to count the total number of records, and one to actually fetch the desired records. By default, associations created on the fly using bindModel() are reset after each query. It depends on the Cake version which query comes first, but I believe that in your case it is the count query; leaving the actual results query without the association. Setting false on on the second argument of bindModel() prevents the association from being reset after the first query.
I'm having trouble composing a CakePHP find() which returns the records I'm looking for.
My associations go like this:
User ->(has many)-> Friends ,
User ->(has many)-> Posts
I'm trying to display a list of all a user's friends recent posts, in other words, list every post that was created by a friend of the current user logged in.
The only way I can think of doing this is by putting all the user's friends' user_ids in a big array, and then looping through each one, so that the find() call would look something like:
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => array(
'OR' => array(
$user_id_array[0],$user_id_array[1],$user_id_array[2] # .. etc
)
)
)
));
I get the impression this isn't the best way of doing things as if that user is popular that's a lot of OR conditions. Can anyone suggest a better alternative?
To clarify, here is a simplified version of my database:
"Users" table
id
username
etc
"Friends" table
id
user_id
friend_id
etc
"Posts" table
id
user_id
etc
After reviewing what you have rewritten, I think I understand what you are doing. Your current structure will not work. There is no reference in POSTS to friends. So based on the schema you have posted, friends CANNOT add any POSTS. I think what you are trying to do is reference a friend as one of the other users. Meaning, A users FRIEND is actually just another USER in the USERS table. This is a self referential HABTM relationship. So here is what I would propose:
1- First, make sure you have the HABTM table created in the DB:
-- MySQL CREATE TABLE users_users ( user_id char(36) NOT NULL,
friend_id char(36) NOT NULL );
2- Establish the relationships in the User model.
var $hasAndBelongsToMany = array(
'friend' => array('className' => 'User',
'joinTable' => 'users_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'friend_id',
'unique' => true,
),
);
var $hasMany = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'user_id'
),
);
3- use the scaffolding to insert a few records, linking friends and adding posts.
4- Add the view record function to the Users controller:
function get_user($id)
{
$posts = $this->User->find('first', array(
'conditions' => array('User.id' => $id),
'recursive' => '2'
));
pr($posts);
}
5- Now you can query the User table using recursive to pull the records using the following command:
http://test/users/get_user/USER_ID
6- Your output will show all of the records (recursively) including the friends and their posts in the returned data tree when you pr($posts)
I know this is a long post, but I think it will provide the best solution for what you are trying to do. The power of CakePHP is incredible. It's the learning curve that kills us.
Happy Coding!
If Post.user_id points to Friend.id (which wouldn't follow the convention btw) then it would be
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => $user_id_array
)
);
which would result in .. WHERE Post.user_id IN (1, 2, 3) ..
Depending on your setup, it might be quicker to run two queries rather than trying to chain them together via the Cake stuff. I'd recommend adding something like getFriendsPosts() in the Users model.
<?php
class UserModel extends AppModel {
// ... stuff
function getFriendsPosts( $user_id )
{
$friends = $this->find( ... parameters to get user IDs of all friends );
// flatten the array or tweak your params so they fit the conditions parameter. Check out the Set class in CakePHP
$posts = $this->find( 'all', array( 'conditions' => array( 'User.id' => $friends ) ) );
return $posts;
}
}
?>
Then to call it, in the controller just do
$friends = $this->User->getFriendsPosts( $this->Auth->User('id') );
HTH,
Travis
Isn't CakePHP already generating the efficient code of:
SELECT * from Posts WHERE user_id IN (id1, id2 ...)
if not, you can do
$conditions='NULL';
foreach($user_id_array as $id) $conditions.=", $id";
$posts = $this->Posts->find('all', array(
'conditions' => "Post.user_id IN ($conditions)",
));
If your models are properly associated, Cake will automatically retrieve related model records. So, when you search for a specific user, Cake will automatically retrieve related friends, and related posts of these friends. All you need to do is set the recursion level high enough.
$user = $this->User->find('first', array('conditions' => array('User.id' => $id), 'recursive' => 2));
debug($user);
// gives something like:
array(
User => array()
Friend => array(
0 => array(
...
Post => array()
),
1 => array(
...
Post => array()
)
)
)
All you need to do is extract the posts from the user's friends, which is as easy as:
$postsOfFriends = Set::extract('/Friend/Post/.', $user);
How to write this query using the find statement in cakephp
$this->Form->query("Select id from forms order by id DESC LIMIT 1")
This should do it:
$this->Form->find('all', array(
'fields' => array('Form.id'),
'order' => 'Form.id DESC',
'limit' => 1
));
Have a look at the documentation: http://book.cakephp.org/view/73/Retrieving-Your-Data
I agree with the first response here,
2 things I would like to mention
I assume you have a model called Form,Now Cakephp has its own FormHelper and sometimes there maybe a conflict of names, I got this error when I had created a FilesController in my project,
Also you will get an array back which will be of the form
$result['Form] => array(
[id] => 1
[name] => whatever)
and so on, you can easily use this to get whatever data you want.
While this should be find('first'), the real issue is probably even simpler. I can only assume the only reason for doing such a query is to get the last inserted id. In the same "run" as a save you can get the inserted id in the model property by the same name:
$this->ModelName->create($this->data);
if ($this->ModelName->save()) {
$this->Session->setFlash('Saved it!');
$id_of_new_row = $this->ModelName->id;
$this->redirect(array('action' => 'view',$id_of_new_row));
}
As paolo said, but adding 'recursive' to the query:
$this->Form->find('all', array(
'fields' => array('Form.id'), // just return the id, thank you
'order' => 'Form.id DESC', // sort the query result by id DESC
'limit' => 1, // gimme the top id
'recursive' => -1, // don't scan associated models in the query
));
but I'd also use
$this->Form->find('first', array(
'fields' => array('Form.id'),
'order' => array('Form.id DESC'),
'recursive' => -1,
)
);
Which is not much shorter, but is more expressive of what you want.
And I'd suggest you take care, because there's a form helper already, and confusion could happen. On the other hand, you use the $form variable in views, generally, while this is controller code.