Cakephp habtm deep conditions - cakephp

suppose you have those model relations:
Offer HABTM Category
How would you build the find conditions to find all the categories that have at least one offer. In the same time the Offer should be Offer.enabled => 1. So find all the categories with at least one enabled offer. The conditions to check in the offer model are several, but once I can check for the enabled, I think I will be able to check for any other field.
I could bind the habtm model to Category, but in this case I can't check the Offer.enabled condition. Maybe somehow using containable?
The sql query would be :
SELECT DISTINCT Category.nome from categories as Category
LEFT JOIN categories_offers AS CategoriesOffer
ON (CategoriesOffer.category_id = Category.id)
LEFT JOIN offers as Offer ON (CategoriesOffer.offer_id = Offer.id)
WHERE Offer.enabled = 1
Thank you

Use the conditions key in your relationship definition to set the relationship up only if Offer.enabled = 1. Taken from the book. eg:
class Category extends Model {
public $hasAndBelongsToMany = array(
'Offer' => array(
'conditions' => array( 'Offer.enabled' => 1 )
)
);
}

Related

CakePHP 2 Models hasMany as hasOne

I am trying to do this as MVC / CakePHP 2 as possible so if my approach is the incorrect, I would love to know (still learning). I feel like what I am doing should happen in the model and less so in the controller (to follow the fat model skinny controller principals).
I have a hasMany relationship between two tables:
trainings hasMany days.
If I want all the days in a training, this setup works as expected.
But I want (in every instance of training) the first day in a training. My thought process was to setup a hasOne relationship in the Training model as follows:
public $hasOne = array(
...
'FirstDay' => array(
'className' => 'Day',
'foreignKey' => 'training_id',
'fields' => 'FirstDay.training_date',
'order' => array('FirstDay.training_date ASC'),
)
);
In essence training hasOne days as FirstDay.
I assumed that with this setup, if I call a Training object I will get the associated FirstDay.
Instead I get multiple entries for Training -- one for each instance of days for a given training. The SQL that gets output is as follows:
SELECT `Training`.`id`, `Training`.`course_id`, `Course`.`name`, ... `FirstDay`.`training_date`
FROM `tst`.`trainings` AS `Training`
LEFT JOIN `tst`.`courses` AS `Course` ON (`Training`.`course_id` = `Course`.`id`)
...
shortened for your benefit
...
LEFT JOIN `tst`.`days` AS `FirstDay` ON (`FirstDay`.`training_id` = `Training`.`id`)
WHERE 1 = 1 ORDER BY `FirstDay`.`training_date` ASC LIMIT 20
I was assuming that the hasOne would put a limit 1 instead of 20 in the above clause. Since it did not, I tried adding a 'limit' => 1 but that didn't work and the documentation does not mention that as an option in a hasOne relationship. I also do not understand why WHERE 1 = 1 is there but I figure it does not matter since it is a true statement that does not limit anything -- just seems like unnecessary lifting.
The relationship type hasOne is implemented with a LEFT JOIN, and therefore can't support LIMIT as an option, as it would affect the whole query (limiting not only Day but Training too).
There are several approaches to your problem. The most simple is to define your association as hasMany, but setting 'limit'=>1.
public $hasMany = array(
'FirstDay' => array(
'className' => 'Day',
'foreignKey' => 'training_id',
'fields' => 'FirstDay.training_date',
'order' => array('FirstDay.training_date ASC'),
'limit' => 1
)
);
Optionally, you then get rid of the extra numerical index [0] by using Hash::map() after the find():
$this->Training->contain('FirstDay');
$trainings=$this->Training->find('all');
$trainings = Hash::map($trainings, "{n}", function($arr){
$arr['FirstDay']=$arr['FirstDay'][0];
return $arr;
});
For other possible options, see:
Last x blog entries - but only once per user
Method for defining simultaneous has-many and has-one associations between two models in CakePHP?

CakePHP containable behaviour not firing

First time using Cake and its containable behaviour, but it's not working as expected ... or at all.
I'm trying to obtain a list of accessories for a product. Product model HABTM products (alias 'ProductRelation'). Join table is products_products which has two product ids - product_id and related_id. It's against this I want to pull the list of accessories (products driven from the related_id column) for a given product_id
In my Product model, I've added $actsAs = array('Containable');
And in my controller, a quick test of containable using reference from the cookbook fails to contain products at all, even without conditions.
debug($this->Product->find('all', array('contain' => 'ProductRelation')));
.. returns an array of every product in the db, with ALL related models - images, content tabs, ratings, reviews, pricing, etc I haven't tried applying any 'conditions' against this because the call as written should limit data to the product and it's ProductRelation data, according to the cookbook ...
Any tips?
It seems like you have recursive on. Try using the following:
debug($this->Product->find('all', array(
'contain' => 'ProductRelation',
'recursive' => -1
)));
If that works for you, you should start adding containable to the AppModel class and setting the recursive property to -1. This will ensure you only ever get the results you request.
NB: Cake does not join for HABTM, so you can not use ProductRelation in any conditions.

how to get books from one category excluding other category (through cross table)

For now I am using following code to get books from certain category:
$options['conditions']['Category.id'] = $category_id;
$options['joins'] = array(
array(
'table' => 'books_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
$this->Book->find('all',$options);
This just finds all books from given category_id.
So there are 3 tables: categories,books and books_categories. books_categories has two fileds: book_id and category_id so basicly its just HABTM relation. The problem is that one book may belong to many categories and I want for example find all books from category 5 but excluding books from categories 5,6 and 7. How I can do this?
edit -------------------------------------------------------------------------------
Ok so I figured out how it should look in pure SQL - the conditions should be like this:
where
category_id = <given category>
and books.book_id not in
(
select book_id from book_categories
where category_id in (<given set of cat>)
)
order by books.inserted
this will get all books from one category but excluding books from a set of other categories.
Now I want to force Cake to generate similar SQL query.
I tried so far:
$options['conditions']['Category.id'] = $category_id;
$options['conditions']['AND'][] = 'Book.id NOT IN (SELECT id_book FROM book_categories WHERE kat IN (133,134))';
$options['order'] = array('Book.inserted' => 'desc');
$options['joins'] = array(
array(
'table' => 'book_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
This generates this query (sory - table names are little bit different):
SELECT `Book`.`id`, `Book`.`OK`, `Book`.`price`, `Book`.`link`, `Book`.`title`,
`Book`.`author`, `Book`.`img`, `Book`.`inserted`, `Book`.`checked`, `Book`.`big_img`, `Book`.`lang`, `Book`.`asin`, `Book`.`description`, `Book`.`last_update`, `Book`.`review`, `Book`.`changed`
FROM `amazon`.`linki` AS `Book`
inner JOIN `amazon`.`cross_kategorie_full` AS `BookCategory` ON (`Book`.`id` = `BookCategory`.`id_book`)
inner JOIN `amazon`.`kategorie` AS `Category` ON (`BookCategory`.`kat` = `Category`.`id`)
WHERE `Category`.`id` = 4
AND `Book`.`OK` = 'OK'
AND ((`Book`.`big_img` NOT LIKE '%no-image%')
AND (`Book`.`id` NOT IN (SELECT id_book FROM cross_kategorie_full WHERE kat IN (133,134))))
ORDER BY `Book`.`inserted` desc LIMIT 20
But there is error: Maximum execution time of 30 seconds exceeded - so There is something that doesnt end (loop?) in this sql statement...
Update relative to updated question
For the sql to yield correct results (in an acceptable time) you'll need to join with Categories again giving it another alias. Since this leads to another question, I suggest you post it tagged with mysql and query-optimization.
End update
As it is, a HABTM relationship is a bit devious (since it really isn't a HABTM). If you have only one row per book-category match in books_categories you can't know to what other categories a certain book belongs to, so you can't really tell which ones you really want (i.e. don't belong in those other categories). It's CakePHP's data layer and models that solve this problem for you behind the scenes :)
The only solution I see is to use Set::extract to further query the results that you get and filter out Books that belong to Categories that you didn't want to include. (something like:
// returns all books not belonging to categories 3,4
$new_result = Set::extract('/Books/Category[id!=3][!=4]', $results);
On a side note, I find it very useful in cases like this, to query the DB and visualize the complexity of the SQL query that gets you the required results. Also, you should activate the CakePHP debug toolbar to see the SQL queries that are sent to the DB so you have a better idea of what's going on behind the scenes.
The CakePHP book, advises the following at the end of "Associations: Linking models together" Section (emphasis mine).
Using joins allows you to have a maximum flexibility in how CakePHP handles associations and fetch the data, however in most cases you can use other tools to achieve the same results such as correctly defining associations, binding models on the fly and using the Containable behavior. This feature should be used with care because it could lead, in a few cases, into bad formed SQL queries if combined with any of the former techniques described for associating models.
I think there is a typo in there. You want to get all books from category 5 but not from category 5, 6 and 7?
Nevermind. Cake convensions state out that there should be always a primary key within the HABTM table, so you may add a "id" column. With this the next steps are much easier or should I rather say: "They are getting possible".
What you do next is, you create an association model called "BooksCategory".
Use the 'with' index explained here to link your Book and Category models with each other over that new model. Don't forget to use plugin syntax (PluginName.ModelName) in case the linking model belongs to a plugin.
You are now putting two belongsTo associations in the linking model for each of the models you are linking. This makes sure to fetch those while finding.
Next thing you do is:
$this->Book->BooksCategory->find(
'all',
array(
'conditions' => array(
'BooksCategory.category_id' => 5
)
)
);
Finished ;) No you get only the books from category 5.
Greetings
func0der

Possible to have association or not based on main model's data?

I have a nodes table (Node model). I'd like it to be associated to different data types, but only if one if it's field is set to 1.
Example:
My nodes table has a data_article field (tinyint 1). I only want the Node to $hasMany Article IF that field is a 1.
I tried this:
public $hasMany = array(
'Article' => array(
'conditions' => array('Node.data_articles' => '1')
),
);
But I get an error:
Column not found: 1054 Unknown column 'Node.data_articles' in 'where
clause'
Because the association is doing the Article find in it's own query:
SELECT `Article`.`id`, `Article`.`title`, `Article`.`node_id`, ...more fields...
FROM `mydatabase`.`articles` AS `Article`
WHERE `Node`.`data_artiles` = '1'
AND `Article`.`node_id` = ('501991c2-ae30-404a-ae03-2ca44314735d')
Obviously that doesn't work, since the Node table isn't being Joined at all in this query.
TLDR:
Is it possible to have associations or not based on a field in the main model? If not, how else can I keep different data types in multiple tables, and not have to query them all every time?
I don't think that there's a standard way to do this with CakePHP (at least I can't imagine a way). What definitely is possible would be binding associated models dynamically.
So you might query your model without associations by passing the recursive parameter as -1 to the find() method and based on the result unbind the associated models dynamically. Afterwards you would have to query again, for sure. You might build a behavior out of this to make it reusable.
A quite elegant solution would be possible, if Cake could make a two-step query with a callback after the main model was queried, but before associated models are queried, but this isn't possible at the moment.
Edit: There might be a non-Cake way to archieve this more performantly with a custom Query including IF-statements, but I'm not the SQL expert here.
You should do it from the other side:
public $belongsTo = array(
'Node' => array(
'conditions' => array('Node.data_articles' => '1')
),
);

How to hasMany without foreign key in CakePHP?

I have two tables, say:
TAB1
----
id
tab1_md5
TAB2
----
id
tab2_md5
I would like to create a hasMany relation without foreignKey to be able to use cakephp recursive stuff but don't know how to create the relationship.
I've tried with:
var $hasMany = array(
'Tab2' => array(
'className' => 'Tab2',
'foreignKey' => false))
but i don't know what i should specify in condition
EDIT: the relation is tab1.tab1_md5=tab2.tab2_md5
I don't believe you can do a hasMany relationship without using a foreign key. CakePHP, at that point, has no idea how these tables should be related. Without knowing the relationship it can't do any joins to include the associated table data.
It's not possible. Cake must do a separate query to fetch hasMany data. In that separate query it only uses the primary key of the related model. AFAIK there's currently no way to make it use anything but the primary key. So you'll have to do these queries manually:
$tab1s = $this->Tab1->find('all', array(...));
$tab2s = $this->Tab2->find('all', array(
'conditions' => array('Tab2.tab2_md5' => Set::extract('/Tab1/tab1_md5', $tab1s))
));
$grouped = array();
foreach ($tab2s as $tab2) {
$grouped[$tab2['Tab2']['tab2_md5']][] = $tab2;
}
foreach ($tab1s as &$tab1) {
$tab1['Tab2'] = isset($grouped[$tab1['Tab1']['tab1_md5']]) ? $grouped[$tab1['Tab1']['tab1_md5']] : array();
}
Something along these lines. You could do this automatically in an afterFind callback in the model itself to get the Cake automagic effect.

Resources