For the purpose of this question, this is the association tree (all -> means hasMany), all database structure code adheres to CakePHP conventions.
Forum -> Section (forum_id) -> Topic (section_id) -> Reply (topic_id)
I'd like to run a $this->Reply->find query with certain conditions, and I would like the returned $data["Reply"] array to only return replies where they belong to forum_id=X.
For example, I run a $this->Reply->find with certain conditions (these don't matter), and it returns two results with different parents, and when you go up and up until you reach Forum.id (or Section.forum_id), they differ in forum_id.
What I want is to filter the results so they belong to a certain forum_id. Since forum_id is not a field in the Reply model itself, but instead in Section (which is two "layers" up), I can't use a conditions entry to filter the result.
What should I do?
Simple as this:
<?php
$this->Reply->find('all', array(
'joins' => array(
Reply::joinLeft('Topic'),
Topic::joinLeft('Section'),
Section::joinLeft('Forum'),
),
'conditions' => array('Forum.id' => $forumId),
));
https://github.com/tigrang/EasyJoin - This will determine the relationship between the models and create the join arrays for you.
If you don't want to use the plugin, you'll have to specify the joins array manually or rebind the models to be able to use Containable as it would currently create multiple queries rather than joins.
Related
I'm using ContainableBehavior to get deep associations on a find in my CakePHP 2.4 app. However, this is resulting in far too many SQL queries - into the thousands on one page.
To simplify my relations a little, it looks like this:
Post
hasMany
- Sighting
hasMany
- Field
belongsTo
- Fieldtype
Now, when I do a find() specifying these in my contain statement, my DebugKit MySQL log is filled with queries like this, repeated over and over:
SELECT `Fieldtype`.`id`, `Fieldtype`.`name` FROM `dev_db`.`fieldtypes`
AS `Fieldtype` WHERE `Fieldtype`.`id` = 48
Basically, it's doing a manual find for each and every Field's Fieldtype, even when it should already have the data.
Is there a solution for returning this kind of deep association that does not duplicate queries? I've tried using the Linkable behavior, however
That only seems to work with one-to-one relationships (you can't use
Link like Contain to get data from a hasMany relationship)
You have to start with your main model- you can't Contain all your
child records, and then use Linkable to hook up the models they belongTo.
Code:
$this->Paginator->settings['contain'] = array(
'User',
'Sighting' => array(
'Field' => array('Fieldtype')
),
);
I have a Movie model with the following associations (belongsToMany):
Actor
Country
Genre
...
When a form is posted, I have this data (skipping a lot of details here):
'actors' => array(
'Cary Grant',
'Grace Kelly',
...
),
'genres' => array(
'Drama',
...
),
...
I'd like my update()/store() controller function to easily associate these Models.
An actor with the name 'Cary Grant' may or may not exist and may or may not be already associated with the movie I'm editing. Also I could remove him from this movie, so I'd need to remove the association. Same with Genre and everything else.
So I thought I'd do a BaseModel and do all of this only once in there, like this:
1. get the Movie Model's defined associations.
2. check if POST data contains those associations.
3. for each of them, check if they exist (if not create them) and return an array of ids. the column I'm checking is 'name', but it could be configurable.
4. sync() those ids.
For now, I don't need to add more stuff to the related model from the movie form (ex. an actor's birthdate).
I'm stuck at n.1 ($movie->getRelations() only works for existing movies) and in general I'm not sure if this is the right approach. Hints?
use direct relations: $model->actors
http://laravel.com/docs/4.2/eloquent#relationships
All sync() stuff will be done by ORM, so the most complex thing you should do is n.3. You must create diff between $model->actors and $_POST['actors'] and understand what actors you need to insert (create new actors).
I've met the same problem and this is what I do in my project:
Instead of retrieving all defined relations of the models, I white-listing relations that can be updated by adding a static member
Movie::$editableRelations = ['actors', 'genres', 'countries'];
Loop through the post data, and match with the $editableRelations arrays. If the data of a relation exists, following below steps, otherwise we simply do not touch this relation.
Step 3 and step 4 are the same as yours.
NB: My title was quite difficult to word - If you think of a way to word it more concisely I would appreciate that.
I'm using CakePHP and I am making a basic forum system. My models are set out as follows:
ForumSection has many ForumCategories
ForumCategory has many ForumPosts, has one ForumSection
ForumPost has many ForumPosts, has one ForumCategory (NB: ForumPost can be a thread or a reply)
On the index page of the forum, I would like to display the number of posts and replies in each forum category. I can do this by implementing the Containable relationship (which I have) into my ForumSection model and then using the following statement:
$sections = $this->ForumSection->find('all', array(
'contain' => array(
'ForumCategory' => array(
'ForumPost' => array(
'conditions' => array('ForumPost.is_thread', '1')
)
)
)
));
Then, on my view, I can simply echo the count of ForumPosts.
This is, of course, suboptimal -- I could be potentially bringing back thousands upon thousands of rows of data and loading it into memory when in reality I could do a direct SQL along the lines of SELECT COUNT(*) FROM ForumPosts WHERE forum_category_id = x AND is_thread = 1 for count and avoid this. I could even use a Cake function to do this for me but the point stands that it's more efficient than loading the entire table into memory just to count them.
This would, though, require making a loop in my controller (or model potentially but I'd still need to loop the sections in the controller) and meddling with the returned data so as to insert counts into it.
The way I see it, I have two options:
I could, in the view, get the post count as I loop over the categories, e.g.
foreach ($categories as $category):
// get post count
echo post count
I am reluctant to do this since, to my knowledge, this seems to ill-advised on MVC projects and I'm certain there'll be a more optimal approach I have not considered.
Intercept the found data and insert counts before patching the data through to the view. This seems like the most true to MVC way of doing it but it still feels wrong.
My question, ultimately, is does Cake provide any way of including the count of a model's inner relationships without including the entire data set for said relationship? If not, is there an approach that I could take that would be more straightforward/follow conventions more effectively than my suggested two?
I have two models in cakePHP, one is Parts and the other is Orders.
Parts is a simple model with an id column plusaction multiple fields such as customer, part number, description etc. Each part has its own unique id.
Orders is another simple model with a field called *part_id* which represents an id from the Parts model.
In an action, in the Orders controller I wll need to pull all the orders for that particualr view, find information about that order from the Parts database and put it together in an array ready to be passed to the view.
For example that tables might look like:
ORDERS
Part_id id
2 5
1 7
PARTS
customer partnumber description id
Google XHA-V1 widget_1 1
Yahoo YVS-XN widget_5 2
Since I will have 50-100 orders in any one view, I need an efficient way to reference each order's *part_id* in the Parts database and combine this into one array. Its the searching of the Parts database multiple times that I'm unsure how to achieve effectively.
Note: I guess the solution could be cake or pure PHP.
If you have correctly defined the relation in your Order Model, you should be able to retreive all the corresponding Parts
$this->Orders->find('all');
cakephp will do it in two queries:
- a SELECT for all the Orders
- a SELECT for all the Products where product.id in ( list_of_Orders_ids )
and then it will contruct an array in a proper format
If you need an special query, you could also "manually" set the joins for the query, but you'll need to set the recursive to -1:
$this->Orders->find('all',array('recursive'=>-1,
'fields'=>array('Order.*','Part.*'),
'joins'=>array(array('table' => 'parts',
'alias' => 'Part',
'type' => 'INNER', //or left
'conditions' => array('Part.id = Order.part_id')))))
Good Luck!
You can get an array of (all) Orders using the $this->Order->find('all', array('recursive' => 2)); in your controller. Each order also contains the related Parts information.
Imo customer information should be part of the Order, not of Part. Also you might want Orders to consist of multiple Parts.
In an ActiveRecord (CakePHP flavored) setup I have a HasAndBelongsToMany association setup on Videos and Bins: A Bin can store n references to Videos, and Videos can belong to n Bins.
I need to be able to manually set and store the display order of the Videos within a particular Bin (so the client can have his Videos in a particular order within the Bin.) My initial thought is to create a field in Bin that stores an array of Video IDs in the order they need to appear. Is this the most efficient way to handle this?
If so, when I then get the HABTM query result, what is the cleanest/most efficient way to re-order the returned query to match the sorted array of ID's?
The Videos associated with a Bin are returned as an array:
[Video] => Array
(
[0] => Array
(
[id] => 49b2de95-0338-48b9-bc88-01ba35664358
...
)
[1] => Array
(
[id] => 49b1ccad-b770-410e-be46-03a035664358
...
)
Or is there a better way to achieve what I'm trying to do without using HABTM associations?
Thanks in advance -
What to do when HABTM becomes complicated?
By default when saving a HasAndBelongsToMany relationship, Cake will delete all rows on the join table before saving new ones. For example if you have a Club that has 10 Children associated. You then update the Club with 2 children. The Club will only have 2 Children, not 12.
Also note that if you want to add more fields to the join (when it was created or meta information) this is possible with HABTM join tables, but it is important to understand that you have an easy option.
HasAndBelongsToMany between two models is in reality shorthand for three models associated through both a hasMany and a belongsTo association.
Consider this example:
Child hasAndBelongsToMany Club
Another way to look at this is adding a Membership model
Child hasMany Membership
Membership belongsTo Child,
ClubClub hasMany Membership.
These two examples are almost the exact same. They use the same amount and named fields in the database and the same amount of models. The important differences are that the "join" model is named differently and it's behavior is more predictable.
In your example, you need a way to add and remove without editing other users Video links, therefore standard habtm will not suit you very well. Create a model for this "join" similar to the Membership model described above. Further, if you added a weight field, you could use the ordered behavior (that I wrote) to order each set of videos per bin. Then you would use the following fields
id, bin_id, video_id, weight
And set up bin_id as the 'foreign_key' in the behavior configuartion. Good luck
Well I tried to solve just this problem and think I found the simplest solution possible:
When saving a new order of the related models you delete the existing relations and add the new ones in the desired order. This means the ids created run in your sort order and you can simply sort by id to display the order.
public $hasAndBelongsToMany = array(
'Item' => array(
'order' => 'ItemFoldersItem.id ASC, Item.name DESC',
)
);
Pretty simple isn't it?
Don't know about "most efficient" way to implement what you want, that depends on your whole application and what you want to accomplish. I'd just keep in mind that the most simple solution is often the best.
However, I can give you a tip for sorting your Video array! It is fairly easy:
$yourData = Set::sort($yourData, '/Video/id', 'asc');
Take a look at the Set class for more candy. :)