I have the following table schema ( legacy system so I can't change it :-( )
admins:
admin_id
...
admin_auths:
admin_id ( connects to admin_id )
id ( connects to user_id )
auth ( only care about when this is 'user' )
...
users:
user_id ( connects to id )
...
Here is how I managed to get the relationship working for users:
User.php
class User extends ActiveRecord\Model {
static $has_many = [
[ 'admin_auths',
'foreign_key' => 'id', # key in linked table
'primary_key' => 'user_id', # key in "parent" (this) table
],
[ 'parents',
'through' => 'admin_auths',
'foreign_key' => 'id', # key in linked table
'primary_key' => 'user_id', # key in "parent" (this) table
'class_name' => 'Admin'
]
];
}
AdminAuth.php
class AdminAuth extends ActiveRecord\Model {
static $belongs_to = [
[ 'admin' ],
[ 'user',
'foreign_key' => 'id', # key in linked (this) table
'primary_key' => 'user_id', # key in "parent" table
]
];
}
If I call $user->parents or $user->admin_auths this works perfectly!
However I cannot seem to get it working in the Admin.php file:
class Admin extends ActiveRecord\Model implements JsonSerializable {
// relationships
static $has_many = [
[ 'admin_auths' ],
[ 'children',
'through' => 'admin_auths',
'class_name' => 'User'
]
];
...
}
The way it is shown above. This runs the following SQL:
SELECT `users`.* FROM `users` INNER JOIN `admin_auths` ON(`users`.user_id = `admin_auths`.user_id) WHERE `admin_id`=?
The problem with this is that I need to get it to join ON(`users`.user_id = `admin_auths`.id) ( there is no user_id column )
I've tried setting both the foreign_key and primary_key columns. foreign_key changes the where section of the query and primary_key runs no query ( Just SHOW COLUMNS on users and admin_auths ).
TL;DR: How to I customize the join on a PHP ActiveRecord has_many association?
I have two tables Groups and Tracking and they are connected. I wanna delete Groups records, and delete foreign key in Tracking, but i don't know how to delete or update into NULL value this foreign key. I don't wanna delete all records from Tracking. Any Idea? Thanks for help.
protected $_table_name = 'trackingGroup';
protected $_primary_key = 'trg_id';
protected $_has_many = array(
'tracking' => array(
'model' => 'Orm_tracking',
'foreign_key' => 'tr_trgId',
),
);
protected $_belongs_to = array(
'user' => array(
'model' => 'Orm_users',
'foreign_key' => 'trg_uId',
),
);
function delete(){ //my first idea
foreach($this->tracking->tr_trtId->find() as $tracking)
$tracking->delete();
parent::delete();
}
In Tracking Table I have:
tr_id int PK,
tr_uId int FK,
tr_trtId int FK,
tr_trgId int FK,
tr_dateCreate data,
tr_status Char(1)
In TrackingGroups:
trg_Id int PK,
trg_name char(40),
trg_description char(256),
trg_uId int FK
How should delete works: when user want click delete button it should delete record from TrackingGroups and also it should delete or update field tr_trgId in Tracking table, there should be NULL value in tr_trgId.
Your idea is right, to find all tracking entries use find_all() function and then set tr_trgId to null and save them.
Or you can achieve this in MySQL using Forign key events ON delete set null in Tracking table.
protected $_table_name = 'trackingGroup';
protected $_primary_key = 'trg_id';
protected $_has_many = array(
'tracking' => array(
'model' => 'Orm_tracking',
'foreign_key' => 'tr_trgId',
),
);
protected $_belongs_to = array(
'user' => array(
'model' => 'Orm_users',
'foreign_key' => 'trg_uId',
),
);
function delete(){
//my first idea
foreach($this->tracking->tr_trtId->find_all() as $tracking){
$tracking->tr_trgId= null;
$tracking->save();
}
parent::delete();
}
So I have three tables, the users, groups and users_groups which is a join table.
--
-- Table structure for table `groups`
--
CREATE TABLE `groups` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`description` text NOT NULL,
`all_versions_available` tinyint(1) unsigned NOT NULL DEFAULT '1',
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`,`created`,`modified`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=12 ;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(40) NOT NULL,
`email` varchar(80) NOT NULL,
`password` varchar(50) NOT NULL,
`role` varchar(20) NOT NULL,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`fullname` varchar(80) NOT NULL,
`password_token` varchar(40) NOT NULL,
PRIMARY KEY (`id`),
KEY `nickname` (`username`,`email`,`password`),
KEY `role` (`role`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;
-- --------------------------------------------------------
--
-- Table structure for table `users_groups`
--
CREATE TABLE `users_groups` (
`user_id` int(11) unsigned NOT NULL,
`group_id` int(11) unsigned NOT NULL,
KEY `user_id` (`user_id`,`group_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Before I have implemented the HABTM in my Group and User models, the code I have below worked fine, now, I am getting all the data I need but I am unable to save.
So, my Group Model looks like this:
<?php
class Group extends AppModel {
public $hasAndBelongsToMany = array(
'Application' => array(
'className' => 'Application',
'joinTable' => 'applications_groups',
'foreignKey' => 'group_id',
'associationForeignKey' => 'application_id',
'unique' => 'keepExisting',
),
'User' => array(
'className' => 'User',
'joinTable' => 'users_groups',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'unique' => 'keepExisting',
)
);
public $validate = array(
'name' => array(
'required' => array(
'rule' => array('notEmpty'),
'message' => 'Group name is required'
)
)
);
public function saveGroup($id, $name, $description) {
$id = (int)$id;
if ($id) {
$this->id = $id;
}
else {
$this->create();
}
$this->set('name', $name);
$this->set('description', $description);
$this->save();
return $this;
}
public function getAll() {
$options = array('order' => array('Group.name' => 'ASC'));
$data = $this->find('all', $options);
return $data;
}
public function getOne($id) {
$id = (int)$id;
return $this->find('first', array('conditions' => array('Group.id' => $id)));
}
}
My User model looks like this:
<?php
class User extends AppModel {
public $hasAndBelongsToMany = array(
'Group' => array(
'className' => 'Group',
'joinTable' => 'users_groups',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'unique' => 'keepExisting',
)
);
public function getOne($id) {
$this->id = $id;
$data = $this->read(null, $id);
unset($data['User']['password']);
unset($data['User']['password_token']);
if (isset($data['User'])) $data['User']['gravatar_url'] = 'http://www.gravatar.com/avatar/'.md5($data['User']['email']).'.jpg';
return $data;
}
private function addGravatars($data) {
foreach ($data as $key=>$user) {
$data[$key]['User']['gravatar_url'] = 'http://www.gravatar.com/avatar/'.md5($user['User']['email']).'.jpg';
}
return $data;
}
public function getAll() {
$data = $this->find('all', array('order' => array('User.fullname' => 'ASC')));
$data = $this->addGravatars($data);
return $data;
}
public function countAll() {
return $this->find('count');
}
}
I have been using model for the join table:
<?php
class UsersGroup extends AppModel {
public function deleteAllWithGroup($groupId) {
$id = (int)$groupId;
return $this->deleteAll(array('UsersGroup.group_id' => $id), false);
}
public function saveUsersForGroup($users, $groupId=0) {
$this->deleteAllWithGroup($groupId);
$data = array();
foreach ($users as $id=>$user) {
$data[] = array('user_id'=>(int)$id, 'group_id'=>$groupId);
}
$this->saveMany($data);
}
}
And this is my Groups controller:
<?php
class GroupsController extends AppController {
var $uses = array('Group', 'User', 'UsersGroup');
public function index() {
$this->set('groups', $this->Group->getAllWithInfo());
}
public function edit($id=0) {
$this->set('group', $this->Group->getOne($id));
$this->set('usersList', $this->User->getAllWithGroupInfo($id));
if ($this->request->is('post')) {
$group = $this->Group->saveGroup($this->request->data['id'], $this->request->data['name'], $this->request->data['description']);
// Saving users
if (!isset($this->request->data['user']) || empty($this->request->data['user'])) {
$this->UsersGroup->deleteAllWithGroup($group->id);
}
else $this->UsersGroup->saveUsersForGroup($this->request->data['user'], $group->id);
}
}
public function view($id) {
App::uses('Platforms', 'Lib/Platform');
$this->setPageIcon('group');
$this->set('group', $this->Group->getOne($id));
}
public function delete($id) {
$this->Group->delete((int)$id);
return $this->redirect(array('action' => 'index'));
}
}
There is a couple of issues, the system above works if I remove the HABTM configs, second, I don't, for some very specific reasons not using the forms helper to generate the form and unfortunately for the complexity of the code (this is just a little bit) I can't so I have to name everything manually myself (that's where I see the biggest potential for failure) and lastly when I fire this code now I get:
Database Error
Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'deleteAllWithGroup' at line 1
SQL Query: deleteAllWithGroup
Notice: If you want to customize this error message, create app/View/Errors/pdo_error.ctp
So the UsersGroup model is not being registered, nothing changes when I even delete the file, it is trying to use the name of the method I have previously used to delete the old join data as an SQL command. I have tried all the possible suggestions for naming and structure of the data I have found on Stack but failed, furthest I got was when I got only one of the join items to save, always the last one in the array ...
Anyone can help with this one?
Cheers,
O.
Be conventional
The main problems here seem to be caused by being unconventional
Table names
The docs describe the following:
This new join table’s name needs to include the names of both models involved, in alphabetical order, and separated with an underscore ( _ )
As such by default CakePHP will expect the join table for such a relationship to be called groups_users.
Model names
Given the above the join model for the relationship will be GroupsUser. Defining the hasAndBelongsToMany relationship as follows:
public $hasAndBelongsToMany = array(
'Group' => array(
'className' => 'Group',
'joinTable' => 'users_groups',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'unique' => 'keepExisting',
)
);
Means that CakePHP will still try and user a model named GroupsUser giving it the table name users_groups. To forcibly user a different join model it's necessary to define which model to use - with with:
public $hasAndBelongsToMany = array(
'Group' => array(
'className' => 'Group',
'joinTable' => 'users_groups',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'unique' => 'keepExisting',
'with' => 'UsersGroup'
)
);
Though it would be better to rename the join table and the join model, therefore the config could be reduced to the following, as everything else would be the defaults:
public $hasAndBelongsToMany = array(
'Group' => array(
'unique' => 'keepExisting'
)
);
Calls to a model function that don't exist becomes sql queries
Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'deleteAllWithGroup' at line 1
SQL Query: deleteAllWithGroup
All this demonstrates, is that a query was made on a class which did not implement the called function. This can be verified by checking the class of the object:
debug($this->UsersGroup);
// Most likely "AppModel"
Note that the join model itself does not have any associations defined, as such doing this:
$this->UsersGroup->unbind(...);
Will have no effect - the associations are defined on the models User and Group in the question, even if the class UsersGroup were to be loaded - it does not have any associations defined, much less a habtm relationship to something else (which would require a total of 5 tables!)
Finally, and probably most importantly: this function isn't necessary:
HABTM data is treated like a complete set, each time a new data association is added the complete set of associated rows in database is dropped and created again
It won't cause problems to fix the code so the method is called except that the join table records are deleted whether the save succeeds or not with the code in the question; whereas CakePHP's logic will only delete the join table records upon success.
Be wary of creating thin wrapper functions
While there's nothing wrong with creating methods on your models to encapsulate logic - if that logic is very easily expressed using the existing model api all that does is make the code harder for others to read/debug. Code like this:
public function getOne($id) {
$this->id = $id;
$data = $this->read(null, $id);
unset($data['User']['password']);
unset($data['User']['password_token']);
if (isset($data['User'])) $data['User']['gravatar_url'] = 'http://www.gravatar.com/avatar/'.md5($data['User']['email']).'.jpg';
return $data;
}
Can easily be replaced with a find('first') call and adding a afterFind filter to the User model to add gravatar_url keys to returned results. This leads to less and simpler code.
i'm starting with Kohana 3.3 ORM trying to apply it to an existing internal project.
The project is in use, so i can't change the schema's names. The current schema definition is the following:
Table: utente
idUtente VARCHAR PK
nome VARCHAR
// other fields
Table: sessione
idSessione SERIAL PK
idUtente VARCHAR (FK to utente.idUtente)
// other fields
Table: ruolo
idRuolo SERIAL PK
nome VARCHAR
//other fields
Table: ruoloutente
idRuolo PK (FK to ruolo.idRuolo)
idUtente PK (FK to utente.idUtente)
scadenza DATETIME
// other fields
Now i defined custom table name and custom primary key name into the models and if i use ORM::factory('Utente', 'Marco'); (or any other model) everything is going fine.
class Model_Utente extends ORM {
protected $_table_name ='utente';
protected $_primary_key ='idUtente';
protected $_has_many =
array(
'ruoli' => array(
'model' => 'Ruolo',
'far_key' => 'idRuolo',
'foreign_key' => 'idUtente',
'through' => 'ruoloutente',
),
'sessioni' => array(
'model' => 'Sessione',
'far_key' => 'idSessione',
'foreign_key' => 'idUtente',
),
);
// class logic here
}
class Model_Ruolo extends ORM {
protected $_table_name ='ruolo';
protected $_primary_key ='idRuolo';
protected $_has_many =
array(
'utenti' => array(
'model' => 'Utente',
'far_key' => 'idUtente',
'foreign_key' => 'idRuolo',
'through' => 'ruoloutente',
),
);
// class logic here
}
class Model_Sessione extends ORM {
protected $_table_name ='sessione';
protected $_primary_key ='idSessione';
protected $_belongs_to =
array(
'utente' => array(
'model' => 'Utente',
'far_key' => 'idUtente',
'foreign_key' => 'idUtente',
),
);
// class logic here
}
Now from an instance of Utente i execute $this->ruoli->find_all() and $this->sessioni->find_all() but i obtain an empty model on both..
The generated query is correct on both finding, query executed directly in SQL returns 4 results on ruoli and two results on sessioni..
Found a solution
My problem was that i supposed that find() and find_all() methods would also perisist in the caller object the query results instead of only return the result. Many thanks to every one
I'm using kohana 3.2 and I need help with has_many relationship. The table is written empty data...
So, my User_education model look like: http://gyazo.com/218139e52d85718c0d47bb802f0856fe User_personal model : http://gyazo.com/49fd4ab4fb7506cf8b7c608733a70365
and controller: http://gyazo.com/7d13dd3901870d7ad3d62c09e90a9c14 but fields in database still empty
You should specify the foreign key in your models:
class Model_User_Personal extends ORM
{
protected $_has_many = array(
'educations' => array(
'model' => 'user_education',
'foreign_key' => 'user_personal_id',
),
);
}
The same foreign key should be set in Model_User_Education:
class Model_User_Education extends ORM
{
protected $_belongs_to = array(
'user_personal' => array(
'model' => 'user_personal',
'foreign_key' => 'user_personal_id',
),
);
}