HABTM selection seemingly ignores joinTable - cakephp

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

Related

CakePHP find() group and order by date complex conditions

I am trying to get messages with their corresponding users, in order to display a chat list of profiles with their last message in chronological order.
array(
0 => array(
'Recepient' => array(
'id' => ...
'name' => ...
...
),
'Message' => array(
'content' => ...
'created' => ...
...
)
),
1 => ...
)
and in order to retrieve the results, I've written this find() method:
$msgs = $this->Message->find('all', array(
'group' => array('Recepient.id'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
What I have:
message with corresponding "Recepient",
in chronological order
The problem:
the query DOES NOT retrieve the most recent message from $recepient_id/$sender_id combination.
So instead of list of users with the last message, I have a list of users with a message. What's wrong with my query? Thanks for help!
METHOD 2 results
I've created "chat_id" field in the database which is basically recepient_id+sender_id sorted alphabetically (because if user1 send user2 a message user1 is sender, later when user2 responds, he becomes the sender, so sorting will ensure two users will always have the same chat_id).
Than I added DISTINCT to the query:
$this->Message->recursive = 0;
$msgs = $this->Message->find('all', array(
'fields' => array('DISTINCT Message.chat_id','Message.*','Recepient.*'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
and it does NOT work! I am now getting multiple messages for the same conversation.
If I remove Message fields and Recipient fields from the query, I get correct amount of "chats".
'fields' => array('DISTINCT Message.chat_id'),
but that's not the solution.
CakePHP version 2.7.0
MySQL DB
METHOD 3 results
$msgs = $this->Message->find('all', array(
'order'=>'Message.created DESC',
'fields' => 'recepient_id, content, max(Message.created) as max_created',
'group'=>'recepient_id',
// 'contain' => array('Recepient'),
'conditions'=>array( 'chat_id' => $chats )
));
I gave up on single-find method to resolve this, so now 1.I am getting list of chats, 2.I want to find the last message from each chat.
Acording to http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ my find query should work. What happens is
(int) 0 => array(
'Message' => array(
'recepient_id' => '55e6d764-1444-4aad-a909-9042f07f76da',
'content' => '1st msg',
'created' => '2015-09-20 18:24:17',
'created_nice' => '2 hours ago'
),
(int) 0 => array(
'max_created' => '2015-09-20 18:24:28'
)
),
the field max(Message.created) is indeed showing the latest message from conversation but the 0=>Message array is for different message! As you can see $results[0]['Message']['created'] time and $results[0][0]['max_created'] are different!
Finally! Working solution:
$db = $this->Message->getDataSource();
$chats = $db->fetchAll(
'select id from (select * from messages order by created desc) Message
where recepient_id = "'.$pid.'" or sender_id = "'.$pid.'"
group by `chat_id`'
);
Above code will retrieve all messages, following the requirements. You can
a) SINGLE QUERY add mysql JOIN to include associated model (we keep the code single-query neat)
b) TWO QUERIES BUT SMARTER easier method, to select just "ids" of the messages and create another query that will have cake's build in containable behaviour. This might be better also because Behaviours will be applied.
You will have array tree as a result, to dig out the actual ids for next query use following:
$chats = Set::extract("/Message/id",$chats);
c) translate the solution to CakePHP query builder... :) up for a challenge?

cakephp group on assosiated model / table

Short background: I have orders that contains products called 'Komplexes'. Komplexes have different sizes (height and width) if there are multiple Komplexes with the same measures in an order they have to be grouped and a counter must be added to create jobs for the workers.
My Models:
class Order extends AppModel {
public $hasMany = 'Komplex';
public $belongsTo = array(
'Customer' => array(
'counterCache' => true
)
);
}
class Komplex extends AppModel {
public $belongsTo = array(
'Order' => array(
'counterCache' => true
)
);
...<validation and calculations>
}
In my OrdersController I'm starting with
public function orderproductionjob($id = NULL) {
if (!$id) {
throw new NotFoundException(__('Invalid ID'));
}
$order = $this->Order->find('all', array(
'conditions' => array('Order.id =' => $id),
'group' => array('Komplex.height')
));
die(debug($order));
This gives me a database error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Komplex.height' in 'group statement'
The same 'find' without the 'group' gives me the right result (exept the grouping ;-)
So it's pretty obvious that I'm doing something wrong with the group. I could find examples for assosiations and examples for grouping on the web and in the cookbook but this combination wasn't mentioned or likely I haven't found it. As this is my first project with cakephp I'm hoping, that sombody with more experience can help me out.
What I'm trying to archive in SQL:
SELECT orders.id, orders.name, komplexes.width, komplexes.height, count(komplexes.id) as Count
FROM orders, komplexes
WHERE orders.id = 1 AND komplexes.order_id = orders.id
group by komplexes.width, komplexes.height;
Try changing your code to Group on the Komplex model.
$komplex = $this->Order->Komplex->find('all', array(
'fields' => array('Komplex.height', 'Komplex.width', 'Count(*) as `Count`')
'conditions' => array('Komplex.order_id =' => $id),
'group' => array('Komplex.height', 'Komplex.width')
));
FYI
Your SQL statement works because you are guaranteed to only have 1 orders row. It can and most likely will return wrong results if you try to join to more than 1 orders row.
You need to be careful using SQL reserved words in your statement. In your case Count as the aliased column name. You may want to change that. Please note that my code sample has COUNT surrounded by backticks.

CakePHP : 1054 Unknown column 'User.id' in 'field list'

I am learning CakePHP. There, I am stuck with following problem:
I have a table called comments with fields comment_id, comment_title, comment_text, comment_date & user_id.
Also I have my users table with fields user_id, user_name, user_email, created_date.
Then in my User model I am trying to create a hasMany relationship like :
var $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id'
), );
But this giving me an error
1054 Unknown column 'User.id' in 'field list'
I know this is because I don't have an id column in my users table instead I have user_id
Now, my question is : is there a way to fix this without renaming the user_id field of user table? As this will require change in every existing code where I have used user_id ?
Set in your User model:
public $primaryKey = 'user_id';
If you need create some relationship and models, controller, views.. You can use Bake shell, see: http://book.cakephp.org/2.0/en/console-and-shells/code-generation-with-bake.html
To avoid this kind of errors use foreign key constraints in database.
Cakephp automatically add associations between tables.
public $hasMany = array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id',
'conditions' => array('Comment.user_id' => 'User.user_id'),
'dependent' => true
)
);

Retrieving SUM(value) FROM a related table

I have a database table, lets say Person and each person has votes stored in a Vote table.
Each person will have many votes and some of these are up and some of these are down votes so I am not interested in COUNT.
I have the following query to retrieve votes for a single person:
$votes = $this->Vote->query("
SELECT IFNULL( SUM( value ) , 0 ) AS vote_count
FROM `votes`
WHERE person_id = {$person_id}");
Is it possible for me to fetch this information for a Person each time I call
$this->person->find("all")
This can be done using $virtualFields:
In the Vote model I add the following virtual field:
var $virtualFields = array(
'vote_count' => 'IFNULL( SUM( Vote.value ) , 0 )'
);
In the Person model I retrieve this every time by using the following $hasMany relationship:
var $hasMany = array('Vote' => array('fields' => array('vote_count')));
Note that by default it will always retrieve every field in the Vote table, this will result in retrieving the 'vote_count' SUM field by default and will only show one row as expected if you were to write out the query in full.
If you want to retrieve all of the rows and remove the SUM virtual field then you need to specify all the fields (as necessary) in the fields array except the 'vote_count' field e.g.
var $hasMany = array('Vote' => array('fields' => array('id', 'value')));
I would suggest this is better controlled by having a couple of functions in the Person model as such, which will allow you to decide on the fly how you want to retrieve the votes:
function allWithVoteCounts() {
$this->bindModel(array('hasMany' =>
array('Vote' => array(
'fields' => array('vote_count')
)
)
));
return $this->find('all');
}
function allVotes() {
$this->bindModel(array('hasMany' =>
array('Vote' => array(
'fields' => array('id', 'user_id', 'person_id', 'value', 'date')
)
)
));
return $this->find('all');
}
Then in the controller you can call:
$persons = $this->Person->allVotes();
or
$persons = $this->Person->allWithVoteCounts();

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