CakePHP complex find condition using 'and' , 'or' clause - cakephp

My find query is this;
//here $u_id is id of the users table retrieved using session variable
$this->User->Request->find("all",array("conditions" =>array("request.status" => "friends" ,
"OR" => array("request.friend_id" => "$u_id","request.user_id" => "$u_id"))));
the equivalent SQL query is:-
SELECT `Request`.`id`, `Request`.`user_id`, `Request`.`friend_id`, `Request`.`status`, `User`.`id`, `User`.`name`, `User`.`email`, `User`.`password`,
FROM
`myblog`.`requests` AS `Request` JOIN `myblog`.`users` AS `User`
ON
(`Request`.`friend_id` = `User`.`id`)
WHERE
((`request`.`friend_id` = 3) OR (`request`.`user_id` = 3)) AND `request`.`status` = 'friends'
Whereas I want the following line after 'ON' in the above SQL query to get the desired result:
`Request`.`user_id` = `User`.`id` OR `Request`.`friend_id` = `User`.`id`
What changes should I make in the find() method,
or should I change my model?
My tables are:
users(id, name, password, email)
requests(id, user_id(id of the users table),
friend_id(id of the users table), status)

You can specify the condition in your model.
public $belongsTo = array('User' => array('className' => 'Request',
'foreignKey' => 'user_id',
'conditions' => array('OR' => array('Request.user_id' => 'User.id', 'Request.friend_id' => 'User.id')
))
);

Related

CakePHP runs unnecessary queries when retrieving related models

The Event model has following relations:
var $belongsTo = array(
'Project' => array(
'className' => 'Project',
'foreignKey' => 'project_id',
),
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
here is how I list the events depending on the user input:
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array(
'conditions'=>$conditions, 'order' => array('Event.date' => 'asc')));
And here are the 3 queries that are being run:
1 SELECT `User`.`id`, `User`.`name`, `User`.`surname` FROM `scheduling`.`users` AS `User` WHERE `company_id` = 1
2 SELECT `Project`.`id`, `Project`.`name` FROM `scheduling`.`projects` AS `Project` LEFT JOIN `scheduling`.`customers` AS `Customer` ON (`Project`.`customer_id` = `Customer`.`id`) WHERE `Project`.`company_id` = 1
3 SELECT `Event`.`id`, `Event`.`project_id`, `Event`.`user_id`, `Event`.`date`, `Event`.`hours`, `Event`.`minutes`, `Event`.`xhours`, `Event`.`xminutes`, `Event`.`xdetails`, `Event`.`assignment`, `Event`.`start_time`, `Event`.`material`, `Event`.`meter_drive`, `Event`.`time_drive`, `Event`.`start_location`, `Event`.`finish_time`, `Project`.`id`, `Project`.`name`, `Project`.`customer_id`, `Project`.`project_nr`, `Project`.`address`, `Project`.`post_nr`, `Project`.`city`, `Project`.`company_id`, `Project`.`color`, `Project`.`start_date`, `Project`.`finish_date`, `User`.`id`, `User`.`employee_nr`, `User`.`name`, `User`.`surname`, `User`.`email`, `User`.`password`, `User`.`role`, `User`.`phone`, `User`.`address`, `User`.`post_nr`, `User`.`city`, `User`.`token_hash`, `User`.`company_id`, `User`.`car_id`, `User`.`image` FROM `scheduling`.`events` AS `Event` LEFT JOIN `scheduling`.`projects` AS `Project` ON (`Event`.`project_id` = `Project`.`id`) LEFT JOIN `scheduling`.`users` AS `User` ON (`Event`.`user_id` = `User`.`id`) WHERE `user_id` = 1 AND `project_id` = 5 AND `date` >= '2013-07-01' AND `date` <= '2013-12-06' ORDER BY `Event`.`date` asc
In fact, I only need the third query and not the first two. What causes them and how to get rid of them?
By default, CakePHP will try attach associations.
In all 3 models add:
var $actsAs = array('Containable');
This will allow you to "contain" your queries to specific models.
Now you can do you query thus:
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'contain' => true));
Say you did want Users (but not Projects) back you can do:
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'contain' => array('User')));
Other code is responsible
This code:
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc')
));
Is responsible for this query:
SELECT
`Event`.`id`,
...
FROM
`scheduling`.`events` AS `Event`
LEFT JOIN
`scheduling`.`projects` AS `Project` ON (`Event`.`project_id` = `Project`.`id`)
LEFT JOIN
`scheduling`.`users` AS `User` ON (`Event`.`user_id` = `User`.`id`)
WHERE
`user_id` = 1 AND
`project_id` = 5 AND
`date` >= '2013-07-01' AND
`date` <= '2013-12-06'
ORDER BY
`Event`.`date` asc
However there are only user_id and date conditions in the code in the question.
This condition:
`project_id` = 5
Is being added by un-shown code - probably a behavior. Check your code for where the project_id condition is defined, there is the answer.
query #1 is unrelated
The first query does not look to be related to the code in the question at all - there is nothing in the question that requires finding a user's data. To find where that's coming from - you can use a simple technique. Open up the user model and put this in it:
class User extends AppModel {
public function beforeFind() {
debug(Debugger::trace());
debug(func_get_args());
die;
}
}
This will give a stack trace of how the query is being triggered - edit the application code appropriately once you know where it comes from.
query #2 is required
Assuming the query you want is actually correct (find all events for a single project) - there needs to be a way to restrict on project id. If that's not specified explicitly, the second query is looking for a project id by client id - i.e. the query you want depends upon that data.
This will help you.
$events = $this->find('all', array(
'conditions'=>$conditions,
'order' => array('Event.date' => 'asc'),
'recursive' => -1
));
recursive based on the max containment depth
$conditions = array('user_id'=>$id, 'date >=' => $from, 'date <=' => $to);
$events = $this->find('all', array('recursive'=>0,
'conditions'=>$conditions, 'order' => array('Event.date' => 'asc')));
the change in here is the recursive
recursive=>-1 mean will fetch only event
recursive=>0 mean will fetch event + to whom it belongs to, in this case project and user

Rewriting a complex query using CakePHP

I quickly wrote a quite complex (in terms of structure) SQL query manually in CakePHP initially, but now I am trying to rewrite it to run withing the CakePHP find method.
$sql = "SELECT
`users`.`username`,
(SELECT ROUND(SUM(`divisions`.`amount`), 2)
FROM `purchases`
INNER JOIN `divisions`
ON `purchases`.`id` = `divisions`.`purchase_id`
WHERE `purchases`.`user_id` = `users`.`id`
AND `divisions`.`user_id` = `users`.`id`
AND `purchases`.`group_id` = " . $group_id . "
) AS `owed_to`
FROM `users`
INNER JOIN `memberships` ON `users`.`id` = `memberships`.`user_id`
INNER JOIN `groups` ON `memberships`.`group_id` = `groups`.`id`
WHERE `memberships`.`group_id` = " . $group_id . " AND
`users`.`id` != " . $user_id . ";";
Because SQL allows you to apply the WHERE filter across the whole query it becomes very simple. In Cake you cannot just go:
$results = $this->User->find('all', array(
'conditions' => array(
'Membership.group_id =' => $id
),...
I have tried setting joins:
$joins = array(
array('table'=>'memberships',
'alias' => 'Membership',
'type'=>'inner',
'conditions'=> array(
'Membership.user_id = User.id', 'Membership.group_id' => $id)
),
Which works OK for a single layer of recursion, but then models related to membership (such as group) are not subject to the filter.
I can only imagine I am doing something completely wrong.
Basically I am confused, any help would be appreciated.
Futher information
User => HasMany => Purchase, Membership, Division
Membership => BelongsTo => Group, User
Group => HasMany => Membership
Purchase => HasMany => Division
Purchase => BelongsTo => User, Group
Division => BelongsTo => Purchase, User
You can write it like this:
$results = $this->User->find('all', array(
'conditions' => array(
'Membership.group_id' => $id
),...
If User has many Membership and Membership belongs to User, and $this->User->recursive = 1, then it should work

HABTM selection seemingly ignores joinTable

UPDATE #2 -- SOLUTION FOUND:
Turns out my use of this lookup:
$this->User->Group->find(....)
was not what I needed. To pull out a user's groups I needed to use:
$this->User->find('all',array('conditions' => array('User.id' => $user_id)));
< /UPDATE #2>< PROBLEM>
I'm attempting to do a HABTM relationship between a Users table and Groups table. The problem is, that I when I issue this call:
$this->User->Group->find('list');
The query that is issued is:
SELECT [Group].[id] AS [Group__id], [Group].[name] AS [Group__name] FROM [groups] AS [Group] WHERE 1 = 1
I can only assume at this point that I have defined my relationship wrong as I would expect behavior to use the groups_users table that is defined on the database as per convention. My relationships:
class User extends AppModel {
var $name = 'User';
//...snip...
var $hasAndBelongsToMany = array(
'Group' => array(
'className' => 'Group',
'foreignKey' => 'user_id',
'associationForeignKey' => 'group_id',
'joinTable' => 'groups_users',
'unique' => true,
)
);
//...snip...
}
class Group extends AppModel {
var $name = 'Group';
var $hasAndBelongsToMany = array ( 'User' => array(
'className' => 'User',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'joinTable' => 'groups_users',
'unique' => true,
));
}
Is my understanding of HABTM wrong? How would I implement this Many to Many relationship where I can use CakePHP to query the groups_users table such that a list of groups the currently authenticated user is associated with is returned?
UPDATE
After applying the change suggested by ndm I still receive a large array return (Too big to post) which returns all groups and then a 'User' element if the user has membership to that group. I looked at the query CakePHP uses again:
SELECT
[User].[id] AS [User__id],
[User].[username] AS [User__username],
[User].[password] AS [User__password],
[User].[email] AS [User__email], CONVERT(VARCHAR(20),
[User].[created], 20) AS [User__created], CONVERT(VARCHAR(20),
[User].[modified], 20) AS [User__modified],
[User].[full_name] AS [User__full_name],
[User].[site] AS [User__site],
[GroupsUser].[user_id] AS [GroupsUser__user_id],
[GroupsUser].[group_id] AS [GroupsUser__group_id],
[GroupsUser].[id] AS [GroupsUser__id]
FROM
[users] AS [User] JOIN
[groups_users] AS [GroupsUser] ON (
[GroupsUser].[group_id] IN (1, 2, 3, 4, 5) AND
[GroupsUser].[user_id] = [User].[id]
)
Is there an easy way to refine that such that I only receive the group ids & names for the entries I have membership to? I was thinking of using:
array('conditions'=>array('GroupsUser.user_id'=>$user_id))
...but I receive an sql error on the groups table:
SELECT TOP 1 [Group].[name] AS [Group__name], CONVERT(VARCHAR(20), [Group].[created], 20) AS [Group__created], CONVERT(VARCHAR(20), [Group].[modified], 20) AS [Group__modified], [Group].[id] AS [Group__id] FROM [groups] AS [Group] WHERE [GroupsUser].[user_id] = 36 ORDER BY (SELECT NULL)
I think you just misunderstood what the list find type is ment to do.
The query is totally fine, the list find type is used for retreiving a list of records of a single model only, where the models primary key is used as index, and the display field as value.
http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#find-list

Pagination with Containable conditions work with hasOne, but not with hasMany

For example, I have this relationship:
UserContact hasMany Contact
Contact hasOne Info
Contact hasMany Response
And I need to paginate Contact, so I use Containable:
$this->paginate = array(
'limit'=>50,
'page'=>$page,
'conditions' =>array('Contact.id'=>$id),
'contain'=>array(
'Response',
'Info'
)
);
I want to add search by Info.name, and by Response.description. It works perfect for Info.name, but it throws an error if I try using Response.description, saying that the column doesn't exist.
Additionally, I tried changing the relationship to Contact hasOne Response, and then it filters correctly, but it only returns the first response and this is not the correct relationship.
So, for example, if I have a search key $filter I'd like to only return those Contacts that have a matching Info.name or at least one matching Response.description.
If you look at how CakePHP constructs SQL queries you'll see that it generates contained "single" relationships (hasOne and belongsTo) as join clauses in the main query, and then it adds separate queries for contained "multiple" relationships.
This makes filtering by a single relationship a breeze, as the related model's table is already joined in the main query.
In order to filter by a multiple relationship you'll have to create a subquery:
// in contacts_controller.php:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$dbo = $this->Contact->getDataSource();
$subQuery = $dbo->buildStatement(array(
'fields' => array('Response.id'),
'table' => $dbo->fullTableName($this->Contact->Response),
'alias' => 'Response',
'conditions' => $conditionsSubQuery
), $this->Contact->Response);
$subQuery = ' EXISTS (' . $subQuery . ') ';
$records = $this->paginate(array(
'Contact.id' => $id,
$dbo->expression($subQuery)
));
But you should only generate the subquery if you need to filter by a Response field, otherwise you'll filter out contacts that have no responses.
PS. This code is too big and ugly to appear in the controller. For my projects I refactored it into app_model.php, so that each model can generate its own subqueries:
function makeSubQuery($wrap, $options) {
if (!is_array($options))
return trigger_error('$options is expected to be an array, instead it is:'.print_r($options, true), E_USER_WARNING);
if (!is_string($wrap) || strstr($wrap, '%s') === FALSE)
return trigger_error('$wrap is expected to be a string with a placeholder (%s) for the subquery. instead it is:'.print_r($wrap, true), E_USER_WARNING);
$ds = $this->getDataSource();
$subQuery_opts = array_merge(array(
'fields' => array($this->alias.'.'.$this->primaryKey),
'table' => $ds->fullTableName($this),
'alias' => $this->alias,
'conditions' => array(),
'order' => null,
'limit' => null,
'index' => null,
'group' => null
), $options);
$subQuery_stm = $ds->buildStatement($subQuery_opts, $this);
$subQuery = sprintf($wrap, $subQuery_stm);
$subQuery_expr = $ds->expression($subQuery);
return $subQuery_expr;
}
Then the code in your controller becomes:
$conditionsSubQuery = array(
'Response.contact_id = Contact.id',
'Response.description LIKE' => '%'.$filter.'%'
);
$records = $this->paginate(array(
'Contact.id' => $id,
$this->Contact->Response->makeSubQuery('EXISTS (%s)', array('conditions' => $conditionsSubQuery))
));
I can not try it now, but should work if you paginate the Response model instead of the Contact model.

CakePHP $hasMany not pulling the data from the $belongsTo model. Join is not created

I have two tables: internet_access_codes and radacct.
The internet_access_codes hasMany radacct records.
The join is internet_access_codes.code = radacct.username AND internet_access_codes.fk_ship_id = radacct.fk_ship_id
I created 2 models and wanted to use $hasMany and $belongsTo respectively so that the related radacct records would be pulled when getting and internet_access_codes record.
Here's the code:
class InternetAccessCode extends AppModel{
var $name = 'InternetAccessCode';
var $hasMany = array(
'Radacct' => array(
'className' => 'Radacct',
'foreignKey'=> false,
'conditions'=> array(
'InternetAccessCode.code = Radacct.username',
'InternetAccessCode.fk_ship_id = Radacct.fk_ship_id'
),
)
);
}
class Radacct extends AppModel{
var $name = 'Radacct';
var $useTable = 'radacct';
var $belongsTo = array(
'InternetAccessCode' => array(
'className' => 'InternetAccessCode',
'foreignKey' => false,
'conditions'=> array(
'InternetAccessCode.code = Radacct.username',
'InternetAccessCode.fk_ship_id = Radacct.fk_ship_id'
)
),
);
}
When I find() a record from internet_access_codes I expect it to give me all the relevant radacct records as well. However I got an error because it didnt do the join.
Here's the outcome and error:
Array
(
[InternetAccessCode] => Array
(
[id] => 1
[code] => 1344444440
[bandwidth_allowed] => 20000
[time_allowed] => 30000
[expires_at] => 31536000
[cost_price] => 0.00
[sell_price] => 0.00
[enabled] => 1
[deleted] => 0
[deleted_date] =>
[fk_ship_id] => 1
[downloaded_at] => 2011-09-10 22:18:14
)
[Radacct] => Array
(
)
)
Error: Warning (512): SQL Error: 1054: Unknown column
'InternetAccessCode.code' in 'where clause'
[CORE/cake/libs/model/datasources/dbo_source.php, line 684]
Query: SELECT Radacct.id, Radacct.fk_ship_id,
Radacct.radacctid, Radacct.acctsessionid,
Radacct.acctuniqueid, Radacct.username, Radacct.groupname,
Radacct.realm, Radacct.nasipaddress, Radacct.nasportid,
Radacct.nasporttype, Radacct.acctstarttime,
Radacct.acctstoptime, Radacct.acctsessiontime,
Radacct.acctauthentic, Radacct.connectinfo_start,
Radacct.connectinfo_stop, Radacct.acctinputoctets,
Radacct.acctoutputoctets, Radacct.calledstationid,
Radacct.callingstationid, Radacct.acctterminatecause,
Radacct.servicetype, Radacct.framedprotocol,
Radacct.framedipaddress, Radacct.acctstartdelay,
Radacct.acctstopdelay, Radacct.xascendsessionsvrkey FROM
radacct AS Radacct WHERE InternetAccessCode.code =
Radacct.username AND InternetAccessCode.fk_ship_id =
Radacct.fk_ship_id AND Radacct.deleted <> 1
In the app_model I also added the containable behaviour just in case but it made no difference.
Sadly cakephp doesn't work too well with the associations with foreign key =false and conditions. Conditions in associations are expected to be things like Model.field = 1 or any other constant.
The has many association first find all the current model results, then it finds all the other model results that have the current model results foreignKey... meaning it does 2 queries. If you put the conditions it will try to do it anyway but since it didn't do a join your query will not find a column of another table.
Solution
use joins instead of contain or association to force the join you can find more here
an example of how to use join
$options['joins'] = array(
array(
'table' => 'channels',
'alias' => 'Channel',
'type' => 'LEFT',
'conditions' => array(
'Channel.id = Item.channel_id',
)
));
$this->Model->find('all', $options);
Possible solution #2
BelongsTo perform automatic joins (not always) and you could do a find from radaact, the bad thing of this solution, is that it will list all radacct and put its internetAccesCode asociated instead of the internetAccesCode and all the radaact associated.... The join solution will give you something similar though...
You will need to do a nice foreach that organizes your results :S it won't be to hard though....
Hope this solves your problem.

Resources