What I'm trying to do is pull up profile info w/ the profile's comments. I get everything as expected. No error returned, array is formatted perfectly. My concern is the queries that are run. It runs one query for each ID to get its photo (the start of the query is noted in comments in the code below). I guess this is the only way to get photos by user_id? All in separate queries? Is there a better way?
I'm picturing a profile with 40+ comments and it's scary to imagine. Is this where memcached comes in?
$profile = $this->Profile->find('first', array(
'conditions' => array('Profile.url' => $url),
'contain' => array(
'User' => array(
'fields' => array(
'User.id','User.name'
),
'Photo' => array(
'fields' => array(
'Photo.thumbnail','Photo.image'
)
)
),
'Comment' => array(
'User' => array(
'fields' => array(
'User.name'
),
'Photo' => array( // right here
'fields' => array(
'Photo.thumbnail'
)
)
)
)
)
));
edit:
I obviously have a User table, as well as a Comment table. I also have a Photo table that stores URL's of the users images, foreign key = user_id. So while i query all Comments from a specific profile and it's Comment.user_id, I also need to grab the Photo.thumbnail from the Photo table by its user_id.
You can make it much faster using a LEFT JOIN.
edited code has been edited to better respond to the question (two left joins are needed - not one).
SELECT c.text, i.profiles, p.photo_url
FROM comments AS c
LEFT JOIN profiles AS i on c.profile_id = i.id
LEFT JOIN photos AS p ON i.id = p.user_id
WHERE c.post_id = 32
LIMIT 20
This is just a generic SQL but would:
pull all comments (in the comments table) bellowing to article 32
left join the profiles table matching the comment poster id to the comment
pull the first 20 comments
And with only one query you would pull all 30 comments + their associated profiles.
Hope it helps!
You can replace the array key 'contain' with the array key 'link', and it builds the left joins for you - see linkable behavior. The downside is that all relations linked must be from the hasOne or hasMany to the belongsTo relation.
You might also want to look at Nate's article in the Bakery about doing joins "the cake way".
http://bakery.cakephp.org/articles/view/quick-tip-doing-ad-hoc-joins-in-model-find
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'm a bit of a newbie and I'm having trouble getting my cakephp query correct.
I have a users table and users hasmany rotas. The rotas table looks like
monday_am
monday_pm
tuesday_am
etc
and if the user is due to be in on monday_pm for example, then that field will have 1. Otherwise 0.
I want to get all the users who belong to a particular room, have deregistered=0 and - and this is where I'm having the problem - who have a rota where (for example) tuesday_am = 1
So I have the code:
$options['conditions'] = array('User.room_id'=>$room_id, 'User.deregistered'=>0, 'User.request'=>0);
$options['joins'] = array(
array('table' => 'rotas',
'alias' => 'rota',
'type' => 'INNER',
'conditions' => array('rota.'.$todaysDay.'_'.$amPm => 1)
)
);
$usersToday = $this->User->find('all', $options);
If I don't try the join then I get a nice small list of users like I expect. If I try the join I get 100s of results which are mostly duplicates!
How can I get just one record of a user who has the required conditions in the Users table as well as the Rotas table, and how come doing the join as I have causes so many results?!?
thanks
Assuming you have a column in your rota table called user_id, you can make your Rota model like this:
class Rota extends Model
{
public $belongsTo = array('User');
}
Then in your controller, you can query the Rotas and get the user from it.
$users = $this->Rota->find('all', array(
'fields' => array('DISTINCT (User.id)' /* add more fields for selection */ ),
'conditions' => array(
'User.room_id'=>$room_id,
'User.deregistered'=>0,
'User.request'=>0,
'Rota.'.$todaysDay.'_'.$amPm => 1
)
));
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.
Tables
restaurants
cuisines
cuisines_restaurants
Both restaurant and cuisine model are set up to HABTM each other.
I'm trying to get a paginated list of restaurants where Cuisine.name = 'italian' (example), but keep getting this error:
1054: Unknown column 'Cuisine.name' in 'where clause'
Actual query it's building:
SELECT `Restaurant`.`id`, `Restaurant`.`type` .....
`Restaurant`.`modified`, `Restaurant`.`user_id`, `User`.`display_name`,
`User`.`username`, `User`.`id`, `City`.`id`,`City`.`lat` .....
FROM `restaurants` AS `Restaurant` LEFT JOIN `users` AS `User` ON
(`Restaurant`.`user_id` = `User`.`id`) LEFT JOIN `cities` AS `City` ON
(`Restaurant`.`city_id` = `City`.`id`) WHERE `Cuisine`.`name` = 'italian'
LIMIT 10
The "....." parts are just additional fields I removed to shorten the query to show you.
I'm no CakePHP pro, so hopefully there's some glaring error. I'm calling the paginate like this:
$this->paginate = array(
'conditions' => $opts,
'limit' => 10,
);
$data = $this->paginate('Restaurant');
$this->set('data', $data);
$opts is an array of options, one of which is 'Cuisine.name' => 'italian'
I also tried setting $this->Restaurant->recursive = 2; but that didn't seem to do anything (and I assume I shouldn't have to do that?)
Any help or direction is greatly appreciated.
EDIT
models/cuisine.php
var $hasAndBelongsToMany = array('Restaurant');
models/restaurant.php
var $hasAndBelongsToMany = array(
'Cuisine' => array(
'order' => 'Cuisine.name ASC'
),
'Feature' => array(
'order' => 'Feature.name ASC'
),
'Event' => array(
'order' => 'Event.start_date ASC'
)
);
As explained in this blogpost by me you have to put the condition of the related model in the contain option of your pagination array.
So something like this should work
# in your restaurant_controller.php
var $paginate = array(
'contain' => array(
'Cuisine' => array(
'conditions' => array('Cuisine.name' => 'italian')
)
),
'limit' => 10
);
# then, in your method (ie. index.php)
$this->set('restaurants', $this->paginate('Restaurant'));
This fails because Cake is actually using 2 different queries to generate your result set. As you've noticed, the first query doesn't even contain a reference to Cuisine.
As #vindia explained here, using the Containable behavior will usually fix this problem, but it doesn't work with Paginate.
Basically, you need a way to force Cake to look at Cuisine during the first query. This is not the way the framework usually does things, so it does, unfortunately, require constructing the join manually
. paginate takes the same options as Model->find('all'). Here, we need to use the joins option.
var $joins = array(
array(
'table' => '(SELECT cuisines.id, cuisines.name, cuisines_restaurants.restaurant_id
FROM cuisines_restaurants
JOIN cuisines ON cuisines_restaurants.cuisines_id = cuisines.id)',
'alias' => 'Cuisine',
'conditions' => array(
'Cuisine.restaurant_id = Restaurant.id',
'Cuisine.name = "italian"'
)
)
);
$this->paginate = array(
'conditions' => $opts,
'limit' => 10,
'joins' => $joins
);
This solution is a lot clunkier than the others, but has the advantage of working.
a few ideas on the top of my mind:
have you checked the model to see if the HABTM is well declared?
try using the containable behavior
in none of those work.. then you could always construct the joins for the paginator manually
good luck!
Cuisine must be a table (or alias) on the FROM clausule of your SELECT.
so the error:
1054: Unknown column 'Cuisine.name' in 'where clause'
Is just because it isn't referenced on the FROM clausule
If you remove the Feature and Event part of your HABTM link in the Restaurant model, does it work then?
Sounds to me like you've failed to define the right primary and foreing keys for the Cuisine model, as the HABTM model is not even including the Cuisine tabel in the query you posted here.
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);