I'm very new to CakePHP. I want to do a query in my database like this
SELECT m.id, l.*, lp.picture_path
FROM member m INNER JOIN listing l ON m.member_id = l.member_id
INNER JOIN listingPicture lp ON l.listing_id = lp.listing_id
WHERE lp.picture_default='1'
I have 3 models in my cakephp : Member, Listing, and ListingPicture each with the following relationship
Member hasMany Listing
Listing hasMany ListingPicture
Listing belongsTo Member
ListingPicture belongsTo Listing
From my Member controller how do I execute the query above ?
I've tried
$this->Member->Listing->find("all")
... which works well but when I added a conditions like this:
$this->Member->Listing->find('all', array(
'conditions' => array('ListingPicture.picture_default'=>'1')));
... I get an error.
Because I'm new to CakePHP, I don't know how to see the error.
Can anyone advise me how I can perform this query?
Make sure to set your model as:
public $actsAs = array('Containable');
Then use CakePHP's containable behavior to include only the associated data you want, with specified fields and conditions.
$this->Member->Listing->find('all', array(
'fields' => array('*'),
'contain' => array(
'Member' => array(
'fields' => array('id')
)
'ListingPicture' => array(
'conditions' => array('ListingPicture.picture_default' => '1')
'fields' => array('picture_path')
)
)
));
To follow with the MVC concept, it's suggested to keep your finds in a Model as opposed to a controller. It's not required, but - it makes it much easier to know exactly where all finds are, and keeps with the "Fat model / Skinny controler" mantra. In this case, it'd be something like:
//in the Member Controller
$listings = $this->Member->Listing->getListings();
//in the Listing Model
function getListings() {
$listings = $this->find('all', ...
return $listings;
}
You should give the condition at the time of binding ListPicture to Listing.
$this->Member->Listing->bindModel(array(
'ListPicture'=>array(
'condtions'=>array('ListingPicture.picture_default'=>'1')
))
);
Related
I create a search on models like this:
$options = array(
'conditions' => array(
'CompletedSurvey.' . $this->CompletedSurvey->primaryKey => $id
),
'recursive' => 5
);
$survey = $this->CompletedSurvey->find('first', $options);
The way I have my models set up, this will return five models (do to their various joins) each recursed up to 5 times (if available). The problem is that I only want one of these models to be recursed X5. The others don't need to be.
Is there a way to tell the find function which tables to recurse and at what level to recurse them to? So, telling cake which models to recurse and at what level for each one?
Please read the CakePHP cookbook regarding the recursive property on models, as it does not work the way you are thinking. http://book.cakephp.org/2.0/en/models/model-attributes.html#recursive
What you're looking for is the containable behavior, where you can specify exactly which models to return. Please see the cookbook on how to use containable. http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
Well, this is not exactly recursive, but the only way to access deep relations and avoid to use recursive on a main model is to use containable behavior:
$options = array(
'conditions' => array(
'CompletedSurvey.' . $this->CompletedSurvey->primaryKey => $id
),
'contain' => array(
'SomeModel.SomeOtherModel.AnotherModel.AnotherModel'
)
);
$survey = $this->CompletedSurvey->find('first', $options);
And don't forget to set containable behavior for CompletedSurvey model!
class CompletedSurvey extends AppModel {
public $actsAs = array('Containable');
}
I just confused because of a find() result. This is my configurations:
First, users can have different User.role values: student, admin, and some others.
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('User.role' => 'student')
);
);
When I chain Models like $this->Book->Student->find('list'); I was expecting to get only users whose role are 'student', but instead, it gets all users. What is going on here, what is conditions for on association definition, where can it and cannot be used. Any lead would help, thanks.
PS: I am aware that I could put conditions on find(), that's not the issue
There is a difference between associated data and accessing an associated model object. If you access $this->Book->Student you're accessing the Student model and work in it's scope. The conditions in the defined associations work only in the context of the accessed object.
So if you do a find on the Book and list the students for that book:
$this->Book->find('first', array('contain' => array('Student'));
Your code will work correctly. It will find the book plus the first user who has the role stundent. BUT your association is wrong then: It should be hasMany. because why would you filter a book by role if the book just belongsTo one student?
If you want to filter users by their role you can implement a query param that is checked in beforeFind(), pseudocode: if isset roleFilter then add contention to filter by that role from roleFilter.
Or, if you don't need to paginate just create a getStudents() method in the user model that will return a find('list') that has the conditions.
Or Student extends User and put the filter in the beforeFind() and use that model instead of the User model in your Book association.
If you want to filter on model level or per model I think the last one is a good option. Don't forget to set $useTable and $name or the inherited model will cause problems.
you have miss , inside your model.
try this:
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id', //<------ miss
'conditions' => array('User.role' => 'student')
);
);
Yoi can debug your query to check what is the real query that you make.
Personally I have never use this approach, I prefer to use foreign key with another table for examples Rolesand User.role_id.
Is better for me to use this approach to have more flexibility inside your app.
After I prefer to use a conditions where inside controller to check well the query, because in your way every query you search always for student role not for the other and can be a problem for the rest of role, because inside controller you see a find without conditions but it doesn't take right value because in your model there is a particular conditions.
For me the good way is to create a new table, use foreign key and where conditions inside action of the controller to view well what are you doing.
For default all relations are "left join", you must set the parameter "type" with "inner" value
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('Student.role' => 'student'), // <-- Fix That (field name)
'type' => 'inner', // <-- add that
);
);
Tables
User(id)
Profile(id, *user_id*, type)
Attribute(id, *attribute_name_id*, value)
AttributeName(id, name)
ProfileAttribute(id, *profile_id*, *attribute_id*)
Relationships
The relationships are set up correctly (and go both ways, hasMany/belongsTo).
User hasMany Profile
Profile hasMany ProfileAttribute
Attribute hasMany ProfileAttribute
(could be written Profile hasMany Attribute through ProfileAttribute)
AttributeName hasMany Attribute
Goal
For a specified User id, with a find() in the User model, I only want the following fields, laid out as such:
$results[Profile.type][AttributeName.name][Attribute.value]
Is it even possible to retrieve results arranged like this? I've been playing around with Find and Containable for hours, but, first time trying to do anything complicated like this with Cake, I can't get the hang of it.
Thanks!
EDIT
I'm getting these results now, all that I need, but nowhere near the desired format above -can it be done as part of the find, or does it need to be sorted after?
Yep, it's possible. You just have to specify fields on containable:
$this->User->find('all', array(
'conditions' => array('User.id' => $id),
'fields' => array('id'),
'contain' => array(
'Profile' => array(
'fields' => array('id','type'),
'ProfileAttribute' => array(
'fields' => array('id'),
'AttributeName' => array(
'fields' => array('id','name'),
'Attribute' => array(
'fields' => array('id','value')
)
)
)
)
)
);
Be wary that when you use contain and fields options, you have to specify the id so it can make the association (check the docs)
EDIT: I don't know if you can group contained data as the docs didn't say anything about that, but probably you can, as they accept some parameters as in the main query. You can try it, adding group to any contained data that you want to group
I'm trying to get my head around bi-directional self-referential hasMany through relationships in CakePHP (what a mouthful!).
I'm working on a picture matching website.
Pictures are associated to other pictures via a 'match' (the join model).
Each match has two pictures and stores the current rating and the total number of votes.
When viewing a picture, all of its related images from either direction should be available (via its matches).
I've started by defining a hasMany through relationship with a join model.
The pictures_matches join table has this structure:
id | picture_id | partner_id | rating | total_votes
My match join model association looks like this:
class PictureMatch extends AppModel {
...
public $belongsTo = array(
'Picture' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Partner' => array(
'className' => 'Picture',
'foreignKey' => 'partner_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
Each picture needs to be able to access its related pictures from either direction, but this is where my grasp is slipping.
It looks like I need to save both sides of the relationship but this destroys the extra data stored in the join model - with two db entries, voting could vary depending on the direction.
Can anyone shed any light on the best way to do this in CakePHP? I'm rather confused.
Is it possible to create the inverse relationships on the fly?
You can create realtions on the fly vie Model::bindModel(), very usefull stuff this would alow you to bind reverse relations or rather any direction you would like on the fly.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
Also using Containable behaviour you can create infinite chain of retriving your associated date ex.
contain('Picture.PictureMatch.Partner.PictureMatch.Picture.....')
Basically you can loop through all of your models as long as each chain is somehow related to the next one to explain it better simple example ( please disregard logic in it )
Circle belongsTo Square
Square belongsTo Triangle
So Triangle is not related to Circle ( directly ), but Square is kinda in between
Circle->find('all', array('...', contain => array('Square.Triangle'));
or to have more fun lets get circle by circle with loop around
Circle->find('all', array('...', contain => array('Square.Trinagle.Square.Circle'));
and so on, of course those example are useless and without any programming logic, but I hope you understand the point that you can loop trough infinite number of relations going back and forth.
I am not sure if this is the best solution but if you did this:
public $belongsTo = array(
'Picture1' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
),
'Picture2' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
),
'Partner' => array(
'className' => 'Partner',
'foreignKey' => 'partner_id',
),
);
then when you do a search you just search for ($this->data['Picture1'] == $var || $this->data['Picture2'] == $var) and so long as you have recursive set to 1 or 2 you should get back all the related data for that Picture.
I assume this is abandoned, but it's easily resolvable -- it has to do with the phase
destroys the extra data stored in the join model
That means your saves are running a deleteAll and inserting the record on matching... instead you need to find and update that record...
This can be done in a few ways, but the easiest is before your save call, look it up, and include the primary key in the match record data. Basically don't save it as a HABTM, and only save it as a hasMany if you've already tried to find an existing match record primary key (id) and updated the data to save with it.
In my "Reports" controller, which is just a dummy controller without any actual database, I'm trying to generate a paginated view of other models. For example, to generate paginated view of "Transactions" model I'm doing the following:
$this->loadModel('Transactions');
$this->Transactions->bindModel(array('belongsTo'=>array('Item'=>array('className'=>'Item'),'Member'=>array('className'=>'Member'))));
$results = $this->paginate('Transactions',null,array('recursive'=>1));
But this is not giving me associated data from Items and Members. If I do a
$this->Transactions->find('all',array('recursive'=>1))
I get the associated data, but not paginated. How will I get paginated view which includes the associated data too?
Two things: First, even when plural model names can work for some odd reason, the convention is that model names are singular, like $this->loadModel('Transaction');. See the manual on naming conventions.
Second, forget about recursive and go for the Containable behavior. Frankly, it's so useful that I wonder why it isn't the default process (perhaps because Containable got created when the framework was very mature). Matt has a good book explaining why Containable is good (download it, really, it's almost mandatory :D ). But to help even more, I'm going to tell you exactly how you solve your issue with containable:
1) Define the associations in the models, like:
In Transaction model:
var $belongsTo = array(
'Item' => array(
'className' => 'Item',
'foreignKey' => 'item_id',
)
);
In Item model:
var $hasMany = array(
'Transaction' => array(
'className' => 'Transaction',
'foreignKey' => 'item_id',
'dependent' => true,
'exclusive' => true,
)
);
Do the same for the Member model.
2) Create an app_model.php file in /app/ with this code:
(The $actsAs variable here within the AppModel class tells all models to use Containable)
<?php
class AppModel extends Model {
var $recursive = -1;
var $actsAs = array('Containable');
}
?>
3) In the Reports Controller, change the code to something like this:
(The contain parameter is an array of all the associated models that you want to include. You can include only one assoc. model, or all, or whatever you want).
$this->loadModel('Transaction');
$this->paginate = array('Transaction' => array('contain' => array('Item', 'Member')));
$results = $this->paginate('Transaction');
And that's it!