I have three models linked in this manner: Item->Order->Payment
Order hasMany Item
Order hasOne Payment
Now, I am paginating Items and want to add a condition in it to find only items of that order which has payment of a particular id. I hope that makes sense :P
I added the condition as:
array('Payment.id'=>$id)
but it doesn't work. Obviously cause Payment is not associated with Item.
So, how can I go about this?
I am new to cakephp, maybe I am completily wrong but as I understand it you can use other models in your controller with the $uses variable. First make a query on payment model to get your order id, than you can use this id to find the corresponding items.
$uses=array('Item','Order','Payment');
$order_id=$this->Payment->find('first',array('fields'=>'order_id','conditions'=>array('id'=>$payment_id)));
$items=$this->Item->find('all',array('conditions'=>array('order_id'=>$order_id)));
I hope it help.
Why don't you add a condition:
array('Order.payment_id'=>$id)
I think this should work.
If you specify that you want two levels of recursion this should work. Im assuming you have
in Payment.php
//recursion level 1
var $belongsTo = array('Order');
in Order.php
//recursion level 2
var $hasMany = array('Items')
You are right that for paginate to work you must query the model you wish to page and sort the lists by.
in PaymentController.php
//Query two levels deep, so the $payment['Order']['Item'][0-n] will be present
var $paginate = array('recursive' => 2);
Note this method does generate another query for each row to retrieve items.
Make sure the debug level in app/config/core.php is set to 2 to see the database calls.
1) You can use Containable behaviour, in which case you need to put this in your Item model:
var $actsAs = array('Containable');
and this into your Items controller:
$items = $this->Item->find('all'
, array (
'contain' => array('Order' => array('Payment'))
, 'conditions' => array('Payment.id' => $paymentId)
)
)
However I suspect that that will do a left join onto the Payments table (as its a hasMany relationship). So you won't filter Items in any way.
2) If you can't get contains to work then I often use explict joins (read this bakery article by nate on joins) in my find queries. So in your Items controller you'd have:
$items = $this->Item->find('all'
, array (
, 'joins' => array(
array(
'table' => 'payments'
, 'alias' => 'Payment'
, 'type' => 'INNER'
, 'conditions' => array(
'Option.id = Payment.option_id'
)
)
)
, 'conditions' => array('Payment.id' => $paymentId)
)
)
You may also need to specify the join onto the options table.
Related
I have a model named Application. And Application is associated to has_many model named Location.
Application has many Location
In my Application query:
$this->Application->find('all', array('conditions' => 'Application.status' => 'accepted'));
I'm finding applications where status is accepted.
Next thing that I would like to achieve is to find Application records where associated Location is empty/null or in other words where count of Location records is 0.
I tried to make join query like this:
$join_query = array(
'table' => 'locations',
'alias' => 'Location',
'type' => 'INNER',
'conditions' => array(
'Location.application_id = Application.id',
'OR' => array(
array('Location.id' => NULL)
)
)
);
But seems like it's just querying Application records that do have associated Location records.
Thanks in advanced if you guys have any idea(s).
You need to use a left join, not an inner join. Inner join will get only those results that have a row in both of the tables you are joining, where you want only results where there is only a row in the left table. Left joins will get all the results in the left table, regardless if there's a row associated with it in the right table. Then add a condition after the join is complete, to only select those joined results where Location.id is null.
$this->Application->find('all',
array(
'conditions' => array('Location.id' => null),
'joins' => array(
array(
'table' => 'locations',
'alias' => 'Location',
'type' => 'LEFT',
'conditions' => array('Location.application_id = Application.id')
),
),
)
);
Your query says "find any application and its location with application_id = id, AND (1 OR where location.id = null)", so that will match any application that has location.
What I'd do is to leave joins and just use containable and counts. With plain sql I'd use a left join and count the Locations, like in this example. But cake doesn't behave well with not named columns, like "COUNT(*) AS num_locations", so I tend to avoid that.
I'd transform your query to a containtable one
$apps = this->Application->find('all', array('contains'=>'Location'));
foreach($apps as $app) {
if (count($app['Location']) <= 0)
//delete record
}
You could also implement a counterCache, and keep in a BD column the number of locations per application, so the query can be a simple find like
$this->Application->find('all', array('conditions'=>array('location_count'=>0)));
Ooooor, you could add a virtual field with "SUM(*) as num_locations" and then use your join with "left outter join" and compare "num_locations = 0" on the conditions.
Those are the options that comes to mind. Personally I'd use the first one if the query will be a one time/not very used one. Probably put it in the Application model like
public function findAppsWithNoLocations() {
$apps = this->Application->find('all', array('contains'=>'Location'));
foreach($apps as $app) {
if (count($app['Location']) <= 0)
//delete record
}
}
But the other two options would be better if the sum of locations per app is going to be a recurrent query you'll search for.
EDIT
And of course Kai's answer options that does what you want xD. This tendency to complicate things will be the end of me... Well, will leave the answer here to show a reference to other convoluted options (specifically counterCache if you'll need to count the relations a lot of times).
i know this is already some time ago.
i could manage it this way:
public function getEmpty($assoc) {
foreach($this->find('all') as $c){
if(empty($c[$assoc])) $return[] = $c;
}
return $return;
}
now i got all entries that have an empty associated data.
in my controller i call the function like this:
$ce = $this->Company->getEmpty('CompaniesUsers');
companies Users is the Empty Associated model i want to check.
I'm not savvy with MySQL or databases generally, so here's a model of my data (table{cols}]) in order to make my question coherent:
Domains{id, name} Note: 'domains' here does not refer to web domains
Subdomains{id, domain_id, name}
Items{id, subdomain_id, name}
SubdomainsItems{id, subdomain_id, item_id} no domain_id column!
My Items Controller has a function, fetchWithin($domains, $subdomains) which, ultimately, should just execute one of two complexish find(). It's the complexish I can't get past.
Programmatically I can achieve this, but I'm quite certain the better way is by clever joins and the like. Alas, currently this is approach:
If $domainsis empty, do only steps 2&3, otherwise:
foreach($domains as $d): get all the rows of Subdomains where Subdomain.domain_id = Domains.id as $subdomains
foreach($subdomains as $s) : go get all the rows of SubdomainsItems where SubdomainsItems.subdomain_id = Subdomains.id as $item_ids
foreach($items_ids as $i): get all the rows of Items where Items.id = SubdomainsItems.items_id
This works, but I think this is obviating the power of a relational database and I'd like to understand how this should be done (ie. according to either Cakephp convention or simply by whatever MySQL statement would achieve this).
Help would be hugely appreciated, I try to learn the more complex aspects of SQL but it just goes right over my head. :S
Understanding the necessary query
With the structure described in the question the kind of query necessary is of the form:
SELECT
*
FROM
items
LEFT JOIN
subdomains ON (
items.subdomain_id = subdomains.id
)
LEFT JOIN
domains ON (
subdomains.domain_id = domains.id
)
WHERE
domains.name = "foo"
AND
subdomains.name IN ('some', 'list', 'of', 'subdomains');
Compared to the logic in the question this joins all three tables together and permits finding all items by domain name, or subdomain name (or any other criteria involving any or all three tables); Generally speaking if you want to find data in a db and use more than one query to get it - there's a more efficient way to do it.
Implementing the find call
There are a number of ways of creating such a query with Cake. The simplest, probably, is to use the join key and just specify the joins explicitly:
function fetchWithin($domains = null, $subdomains = null) {
$params = array(
'joins' => array(
array('table' => 'subdomains',
'alias' => 'Subdomain',
'type' => 'LEFT',
'conditions' => array(
'Subdomain.id = Item.subdomain_id',
)
),
array('table' => 'domains',
'alias' => 'Domain',
'type' => 'LEFT',
'conditions' => array(
'Domain.id = Subdomain.domain_id',
)
)
)
);
if ($domains) { // single value or an array
$params['conditions']['Domain.name'] = $domains;
}
if ($subdomains) { // single value or an array
$params['conditions']['Subdomain.name'] = $subdomains;
}
return $this->find('all', $params);
}
I have the following tables:
- restaurants (restaurant.id, restaurant.name)
- menus (menu.id, menu.name, menu.active, menu.restaurant_id)
I want to have a list with all restaurants with the active menus (menu.active = true):
- restaurant2
- menu1
- menu4
- restaurant5
-menu3
- restaurant19
- menu34
- menu33
My first idea was something like this:
$options['contain'] = array(
'Menu' => array(
'conditions' => $menuParams //array('Menu.active' => '1') //$menuParams
)
);
This doen't work becaus all restaurants will be listed. I want to have only restaurants with active menus.
Next idea: using join
$options['joins'] = array(
array('table' => 'menus',
'alias' => 'Menu',
'type' => 'RIGHT',
'conditions' => array(
'Menu.restaurant_id = Restaurant.id',
)
)
);
Not good, because, I don't have the ordered list I want. I need the menus grouped by the restaurant. Look above.
Is it the right way to make a join with restaurants and menus(active = true) and then using the contain to get the ordered list? I think that could work but I think also there is an easier way, right?
Any help is welcome! Thank you.
If you are using the ORM machinery from CakePHP, you should have a Restaurant model and a Menu model to describe each table. In the Restaurant model, you should have a $hasMany = "Menu" field, and in menu a $belongsTo = "Restaurant" field (assuming the model names are Menu and Restaurant).
From that point, doing queries using ORM is fairly straightforward:
$this->Restaurant->recursive = 1; // grab the menus
$conditions = array('Menu.active' => '1'); // restrict to active menus only
$this->Restaurant->find('all', array('conditions' => $conditions));
The above in the ad-hoc method of the Restaurant controller should retrieve the rows as an array of Restaurant objects, each bundled with an array of active Menu.
Now I found the easy and clean solution for my concern! Yeah!
First I had to unbind the bindings and then I had to make a new binding with the condition. It works like a charm. Here is the code:
$this->unbindModel(array('hasMany' => array('Menu')));
$this->bindModel(array('hasMany'=>array(
'Menu'=>array(
'foreignKey' => 'restaurant_id',
'conditions' => array(
'Menu.active' => 1
)
)
)));
I thank you all for your answers!
I have a News model, which has a HABTM relationship with an Artists model, and an artist in turn hasMany tourdates.
If I want to find all tourdates related to the current news item, what is an efficient way of phrasing that for CakePHP?
This is what I have so far; I'm wondering if (a) it looks like it should work, and (b) if there's any more concise way of writing it:
$relatedartists = $this->News->ArtistsNews->find('list', array(
'conditions'=>array('ArtistsNews.news_id' => $id),
'fields'=>array('artist_id')
));
$livedates = $this->News->Artists->Tour->find('all', array(
'conditions'=>array('Tour.artist_id'=> $relatedartists,
'date >= ' . time()),
'order'=>'date ASC'
));
What you have is pretty good. I always prefer to use multiple queries rather than use massive joins which create temporary tables. It can reduce performance somewhat.
You might also try something like the below
$opts = array(
'conditions' => array(
'ArtistsNews.news_id' => $id
)
);
$this->News->Artists->recursive = 2;
$this->News->Artists->find('all', $opts);
Something along the likes of this query will also get you what you need (haven't error checked)
How do you use cakephp to count, for example the number of posts, made every month in a year?
Preferably using Model->find('count') and get the data in an array.
I just did something similar, using only CakePHP (no direct queries). It works in CakePHP 2, haven't tested in 1.x.
The code for your example would be something like this:
$params = array(
'recursive' => -1,
'fields' => array('id', 'MONTH(created)')
'group' => array('YEAR(created)', 'MONTH(created)')
);
$numberOfPosts = $this->Model->find('count', $params);
This comes close
Query
$data = $this->Post->query("SELECT COUNT(id),MONTH(created) FROM posts GROUP BY YEAR(created), MONTH(created);");
Return
Array
(
[0] => Array
(
[0] => Array
(
[COUNT(id)] => 1
[MONTH(created)] => 3
)
)
[1] => Array
(
[0] => Array
(
[COUNT(id)] => 2
[MONTH(created)] => 4
)
)
)
When using cake, I prefer to stay as close to the framework as possible. This means that I try to avoid writing queries directly in the controllers because this results in the model code being everywhere. Therefore I recommend one of two solutions
1: (and what I do with more complicated stuff): Create a view for the calculation that you want to do and create a model to match.
2: Use a query as mentioned before, but put it in the model class, not the application class.