I'm currently trying to implement a search engine function in my CakePHP site, trying to return information from 3 tables efficiently. The main usage will be numeric searches, free text will be extremely minimal and as such I'm not trying to optimise for this scenario.
Companies hasMany Products
Products hasMany Prices
Ideally I'd like to be able to use something like the following function in the Products controller (this won't work due to distant relationships):
$results = $this->Product->find('all', array(
//conditions can be defined against all 3 tables
'conditions' =>. array(
'company.name LIKE' => '%'.$search_term.'%',
'product.feature' => $product_feature,
'price.price <' => $price
),
//fields restricted against all 3 tables
'fields' => array(
'company.name',
'product.feature',
'price.price'
)
));
I've tried using Containable behaviour to include the three models but to no avail.
I believe the solution lies with JOINS but my experience with these is limited, I've tried code similar to the following within the find function above:
'joins' => array(
array(
'table' => 'companies',
'alias' => 'Company',
//tried a mix of joins (LEFT, RIGHT, INNER)
'type' => 'LEFT',
'conditions' => array(
'Company.id = Product.company_id'
)
),
array(
'table' => 'prices',
'alias' => 'Price',
//tried a mix of joins (LEFT, RIGHT, INNER)
'type' => 'LEFT',
'conditions' => array(
'Price.product_id = Product.id'
)
),
),
'recursive' => 1,
Edit: The result of the above joins is that when i specify the price in the conditions or fields that it can't be found, I tried changing the names such as Product.Price.price to account for the one to many relationship but still without luck.
I'd appreciate any help in finding a solution!
I prefer less code with cake model/table naming convention (db table products - model name Product, db table prices - model name Price) for further project management. It's looks like You want to do:
$results = $this->Product->find('all', array(
'fields' => array(
'Company.name',
'Product.feature',
'Price.price'
),
'joins' => array(
'LEFT JOIN companies AS Company ON Product.company_id = Company.id
LEFT JOIN prices AS Price ON Product.id = Price.product_id'
),
'conditions' => array(
'Company.name LIKE' => '%'.$search_term.'%',
'Product.feature' => $product_feature,
'Price.price <' => $price
),
));
but if You want to get products with Your all criteria (company and price) only, You should use INNER JOIN, and GROUP BY Product (group option).
Also, if You want to get all products with many prices and companies result and You set/link model relations, You can use contain option, like:
$contain = array(
'Company' => array(
// ...
'conditions' => array('Company.name LIKE' => '%'.$search_term.'%'),
// ...
),
'Price' => array(
// you can set: 'fields' => array('id', ...),
'conditions' => array('Price.price <' => $price),
// you can set order: 'ordder' => '....'
)
);
$this->Product->attach('Containable');
$post = $this->Product->find('all', array(
// ...
'contain' => $contain,
'conditions' => array('Product.feature' => $product_feature),
// ...
));
So You will get all products with feature => $product_feautre, and You get LEFT JOIN companies and prices to this products.
Hope this helps.
Related
I'm trying to join all AffiliateId models for each user through the Cake PHP 2 query builder, I'm seeing my users through the following query, but I get no affiliates despite there being affiliates for some users. I don't want to join affiliates if there are none for a particular user, what am I missing?
$users = $this->User->find('all', array(
'joins' => array(
array(
'table' => 'affiliate_ids',
'alias' => 'AffiliateId',
'type' => 'inner',
'conditions' => array(
'User.id = AffiliateId.user_id'
)
)
),
'order' => array(
'User.username' => 'ASC'
),
'recursive' => -1
));
So, joins will just add a join to the query, it won't select the fields. You can use fields to select them.
However, using Containable and proper associations is the better solution, as it would nest them properly in the result and would also auto select the associated fields:
$users = $this->User->find('all', array(
'contain' => array('AffiliateId'),
'order' => array('User.username' => 'ASC'),
'recursive' => -1
));
hello I have the following models
Restaurant(id, name)
RestaurantMenu(id, name, restaurant_id) [each restaurant has many main menus]
RestaurantMenuItem(id, name, restaurant_menu_id) [Each Menu has many menu items]
I need to find those RestaurantMenu which has at least 1 RestaurantMenuItem. If It has not then it shouldn't have to get the RestaurantMenu also. Atm it is giving me empty array RestaurantMenuItem which I don't want. I don't want an empty object.
$this->Behaviors->attach('Containable');
return $this->find('all', array(
'contain' => array('RestaurantMenu.RestaurantMenuItem'),
'conditions' => array(
'Restaurant.id' => $restaurant_id,
),
'recursive' => 0
));
This could be achieved by applying an INNER join with RestaurantMenuItem on the RestaurantMenu containment. That would cause RestaurantMenu to be selected only if at least 1 associated RestaurantMenuItem row exists, which should satisfy both of your conditions.
Unfortunately there's no joins and group option for containments, so one has to workaround this by for example querying the RestaurantMenu data manually, which should be pretty easy in your case, as you're only selecting a single Restaurant (btw, you should probably use the first finder instead of the all one in that case):
$restaurant = $this->find('first', array(
'conditions' => array(
'Restaurant.id' => $restaurant_id,
),
'recursive' => 0
));
if ($restaurant) {
// RestaurantMenu needs to have the containable behavior attached
// in order for this to work
$restaurant['RestaurantMenu'] = $this->RestaurantMenu->find('all', array(
'contain' => array(
'RestaurantMenuItem'
),
'joins' => array(
array(
'table' => 'restaurant_menu_item',
'alias' => 'RestaurantMenuItem',
'type' => 'INNER',
'conditions' => array(
'RestaurantMenu.id = RestaurantMenuItem.restaurant_menu_id'
)
)
),
'conditions' => array(
'RestaurantMenu.restaurant_id' => $restaurant_id,
),
'group' => array(
'RestaurantMenu.id'
)
));
}
return $restaurant;
or by using the finderQuery option to define a custom query that includes the join:
return $this->find('all', array(
'contain' => array(
'RestaurantMenu' => array(
'finderQuery' => '
SELECT
RestaurantMenu.*
FROM
restaurant_menus AS RestaurantMenu
INNER JOIN
restaurant_menu_items AS RestaurantMenuItem ON
RestaurantMenuItem.restaurant_menu_id = RestaurantMenu.id
WHERE
RestaurantMenu.restaurant_id IN ({$__cakeID__$})
GROUP BY
RestaurantMenu.id
',
'RestaurantMenuItem'
)
),
'conditions' => array(
'Restaurant.id' => $restaurant_id,
)
));
This could also be applied in the RestaurantMenu association configuration in the Restaurant model.
See also
Cookbook > Models > Associations: Linking Models Together > Joining tables
Cookbook > Models > Associations: Linking Models Together > hasMany
I am using this code to make a list of truck types counted and sorted by type.
$count['type'] = $this->Type->find('all',
array('joins' => array(
array(
'table' => 'truck_has_types',
'alias' => 'TruckHasTypes',
'type' => 'LEFT',
'conditions' => array(
'Type.id' => 'TruckHasTypes.types_id',
)
)
),
'fields' => array(
'Type.id',
'Type.name',
'COUNT(TruckHasTypes.types_id) as N'),
'group' => 'TruckHasTypes.types_id'
)
);
The query only returns a single result even thought there should be many more. I found the culprit. The query looks like this (got this from sql_dump)
SELECT `Type`.`id`, `Type`.`name`, COUNT(`TruckHasTypes`.`types_id`) as N FROM `douglass_cake`.`types` AS `Type` LEFT JOIN `douglass_cake`.`truck_has_types` AS `TruckHasTypes` ON (`Type`.`id` = 'TruckHasTypes.types_id') WHERE 1 = 1 GROUP BY `TruckHasTypes`.`types_id`
You can see that
ON (`Type`.`id` = 'TruckHasTypes.types_id')
Does no have the same quotes as
ON (`Type`.`id` = `TruckHasTypes`.`types_id`)
I have added those quotes manually in phpmyadmin and the query is successful, but I cannot not get cakephp to automatically generate this query. Any ideas?
Thank you!
Ryan
Do it this way:
$count['type'] = $this->Type->find('all',
array(
'joins' => array(
array(
'table' => 'truck_has_types',
'alias' => 'TruckHasTypes',
'type' => 'LEFT',
'conditions' => array(
'Type.id=TruckHasTypes.types_id',
)
)
),
'fields' => array(
'Type.id',
'Type.name',
'COUNT(TruckHasTypes.types_id) as N'
),
'group' => 'TruckHasTypes.types_id'
)
);
Instead of writing joins do it "Cake way" — write model association: Associations: Linking Models Together
Also does 'group' => 'TruckHasTypes.types_id' needs to be one level upper?
In CakePHP 2.4, I'm trying to generate conditions for a search query that searches for data that has been tagged across a HABTM relationship.
My problem is twofold: I can't get my query to return only data tagged with ALL of my search terms, and I can't get my query to return results for partial tags.
This loop generates a working query returning data with ANY of the tags in the search query.
foreach($tags as $tag) {
$conditions['Tag.name'][] = $tag;
}
$query = $this->Tagged->getQuery('all', array(
'conditions' => $conditions,
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
I'd like to get the loop to generate conditions returning only data tagged with ALL the search terms, not data tagged with any of them, and to return matches for partial terms.
EDIT 2:
My database schema looks like this (I'm using the CakeDC Tags plugin to add tags)
Posts
id | other_data
public $hasAndBelongsToMany = array(
'Tag' => array(
'with' => 'Tagged'));
Tagged
id | model | foreign_key | tag_id
Tags
id | name
This is the join I'm trying to use: It complains when I use Tag as an alias: Not unique table/alias: 'Tag'.
$joins = array(
array(
'table' => 'tags',
'alias' => 'queryTag',
'type' => 'LEFT',
'conditions' => array(
'queryTag.name' => 'keyword'
)
),
);
$query = $this->Tagged->getQuery('all', array(
//'conditions' => array('Tag.name LIKE' => '%' . $data['tags'] . '%'),
'joins' => $joins,
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
return $query;
Its hard to tell without your full database schema but i think you need to join two tables like-
$options['joins'] = array(
array(
'table' => 'tagged_tags',
'alias' => 'TaggedTag',
'type' => 'LEFT',
'conditions' => array(
'TaggedTag.tagged_id' => 'Tagged.id'
)
),
array(
'table' => 'tags',
'alias' => 'Tag',
'type' => 'LEFT',
'conditions' => array(
'TaggedTag.tag_id' => 'Tag.id'
)
)
);
I have the problem with the sequence of joins. The similar problem was in another question Manipulating Order of JOINS in CakePHP. The answer was to use Containable behavior. In my case that is unacceptable because I have deeper associations and containable generates too many queries. Containable does not generate joins for the three level associations. It generates additional queries for every entry from the second level table.
My query is:
$this->LevelOne->find('all', array(
'joins' => array(array(
'table' => 'level_three',
'alias' => 'LevelThree',
'type' => 'LEFT',
'conditions' => array(
'LevelThree.id = LevelTwo.level_three_field_id'
)
))
));
The problem here is that cake generates several joins but the join of the LevelThree table is done before the joins of the LevelTwo tables and that throws an SQL error "Unknown column 'LevelTwo.level_three_field_id' in 'on clause'". If the LevelThree join would be at the end of the query after all LevelTwo joins the query would be okay.
So, the question is how to change the sequence of joins?
Finally I figured out how to do that:
$this->LevelOne->unbindModel(array('belongsTo' => array('LevelTwo')));
$this->LevelOne->find('all', array(
'joins' => array(
array(
'table' => 'level_two',
'alias' => 'LevelTwo',
'type' => 'LEFT',
'conditions' => array(
'LevelTwo.id = LevelOne.level_two_field_id'
)
),
array(
'table' => 'level_three',
'alias' => 'LevelThree',
'type' => 'LEFT',
'conditions' => array(
'LevelThree.id = LevelTwo.level_three_field_id'
)
)
)
));
To those who has similar problems but in relations a-la $belongsTo, to have correct order you should set it correctly.
For example when you have such code:
var $belongsTo = array(
'Vacancy',
'Applicant' => array(
'className' => 'Person',
),
'Recruiter' => array(
'className' => 'Person',
),
'Company' => array(
'conditions' => array('Company.id = Vacancy.company_id'),
),
);
But in the result always receive results where Vacancy always joins last, you should do juts simple thing: add those "Vacancy" model not as array value, but as a key=>value, as others:
var $belongsTo = array(
'Vacancy' => array(), // Just add empty array here -- all magic is here :)
'Applicant' => array(
'className' => 'Person',
),
'Recruiter' => array(
'className' => 'Person',
),
'Company' => array(
'conditions' => array('Company.id = Vacancy.company_id'),
),
);
Now all will be in straight order: Vacancy, Applicant, Recruiter & only then Company.
Have you thought about creating a HABTM model and inserting your own 'finderQuery' to override the model query?
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
Sometimes with complex queries, it make more sense to create the custom query so cake doesn't have to deal with it. To do that, you can just create the function in the model and then call it like you would any other query.
function customJoin(){
return $this->query('CUSTOM QUERY HERE');
}
Then call it from the controller:
$this->ModelName->customJoin();
I think sometimes we rely too heavily on the automation that a framework provides and forget that WE are in control and can implement code outside of the core. I do it all the time. Hope this helps.
public function selectdata(){
$option= $this->Group->find('all',
array(
'joins' =>
array(
array(
'table'=> 'user_groups',
'alias'=>'g',
'type'=> 'INNER',
'conditions'=> array('g.group_id =Group.id ')
),
array(
'table'=> 'users',
'alias'=>'u',
'type'=> 'INNER',
'conditions'=> array('u.id = g.user_id')
),
),'fields'=>array('Group.name,Group.created,Group.modified,Group.status,Group.created_by,u.first_name,u.last_name'),
'conditions'=>array('g.group_id=Group.created_by or g.user_id=u.id')
// ."'".$created_by."'"
)
);
var_dump($option);//die();
if(isset($this->params['requested']))
{
return $option;
}
$this->set('groups', $option);
}