We are converting an application for use with CakePHP 2.0.3.
For some reason, I cannot seem to set proper relations between my models.
Here's an example:
User (id, petid, country, picid, ...)
Pet (id, userid, picid, ...)
Picture (id, albumid, ....)
Album (id, userid, petid, ...)
The meanings of these are the following:
- A user can have multiple pets, but can only have selected one pet at the same time (therefore, petid in User)
- Pets belong to one user
- Pets and Users can have multiple pictures, but only one profile picture, therefore Pet.picid and User.picid
- Pets and users can have multiple Albums
I set up my models in CakePHP, but I cannot figure out which relations to use between them since the Database is not following the conventions.
I've tried the following:
User
-> hasMany(Pets)
-> hasOne(Picture)
-> hasMany(Album)
Pet
-> belongsTo(User) (works fine, with foreignkey userid)
-> hasMany(Album)
-> hasOne(Picture)
Album
-> hasMany(Picture)
---- Logic to achieve this? It either belongs to a user or pet-----
-> belongsTo(User)
-> belongsTo(Pet)
Picture
-> belongsTo(Album)
I'm new to CakePHP and cannot figure out the way to go here.
Do you have any suggestions?
I would suggest using Aliases in your relationships which will help get your head around the data being returned.
For example, your User model could use SelectedPet and ProfilePicture in it's associations:
User.php model
/**
* belongsTo associations
*
* #var array
*/
public $belongsTo = array(
'SelectedPet' => array(
'className' => 'Pet',
'foreignKey' => 'petid'
),
'ProfilePicture' => array(
'className' => 'Picture',
'foreignKey' => 'picid',
)
);
/**
* hasMany associations
*
* #var array
*/
public $hasMany = array(
'Album' => array(
'className' => 'Album',
'foreignKey' => 'userid',
'dependent' => false
),
'Pet' => array(
'className' => 'Pet',
'foreignKey' => 'userid',
'dependent' => false
)
);
Your Pet model could use ProfilePicture as well:
/**
* belongsTo associations
*
* #var array
*/
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'userid'
),
'ProfilePicture' => array(
'className' => 'Picture',
'foreignKey' => 'picid'
)
);
/**
* hasMany associations
*
* #var array
*/
public $hasMany = array(
'Album' => array(
'className' => 'Album',
'foreignKey' => 'petid',
'dependent' => false
)
);
Picture model:
/**
* belongsTo associations
*
* #var array
*/
public $belongsTo = array(
'Album' => array(
'className' => 'Album',
'foreignKey' => 'albumid'
)
);
..and finally your Album model:
/**
* belongsTo associations
*
* #var array
*/
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'userid'
),
'Pet' => array(
'className' => 'Pet',
'foreignKey' => 'petid'
)
);
/**
* hasMany associations
*
* #var array
*/
public $hasMany = array(
'Picture' => array(
'className' => 'Picture',
'foreignKey' => 'albumid',
'dependent' => false
)
);
With regards to the logic of an Album belonging to a User or a Pet, you could just handle this in your controller when saving data or returning it. I.e User is given preference over Pet.
I hope this helps.
Related
I would like to create a new model using another parameter
class Beacon extends AppModel {
/**
* Display field
*
* #var string
*/
public $displayField = 'name';
/**
* Validation rules
*
* #var array
*/
public $validate = array(
'UUID' => array(
'uuid' => array(
'rule' => array('uuid'),
),
),
);
//The Associations below have been created with all possible keys, those that are not needed can be removed
/**
* hasOne associations
*
* #var array
*/
public $hasOne = array(
'Position' => array(
'className' => 'Position',
'foreignKey' => 'beacon_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
I would like to ping this beacon passing in parameter the beacon UUID not the primary key.
class Ping extends AppModel {
//The Associations below have been created with all possible keys, those that are not needed can be removed
/**
* belongsTo associations
*
* #var array
*/
public $belongsTo = array(
'Beacon' => array(
'className' => 'Beacon',
'foreignKey' => 'beacon_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
How do I create a Ping object without the beacon primary_key but the UUID ?
on the ping controller I would like to add
public function pingBeaconWithUUID($uuid) {
How do I manage that ?
Thanks !
If the UUID is unique you can use it as value for the id field. The id needs not to be an integer although it is preferable.
But I would also propose to use the approach by AgRizzo and look up the id.
Will the given below code can work where Mark and Subject are two model with which Student model associate through hasMany association
class Student extends AppModel
{
public $name = 'Student';
var $hasMany = array(
'Mark' => array(
'className' => 'Mark',
'foreignKey' => 'student_id'
),
'Subject' => array(
'className' => 'Subject',
'foreignKey' => 'student_id'
)
);
}
yes you can relationship with many tables
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
Find conditions with hasMany model
I have the following models: Attachment and PurchaseOrder hence the datatables attachments and purchase_orders.
In PurchaseOrder, I have
class PurchaseOrder extends AppModel {
public $hasMany = array(
'Pdf' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Pdf.model' => 'PurchaseOrder'
)
),
'Zip' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Zip.model' => 'PurchaseOrder'
)
),
In Attachment, I have the following:
public $belongsTo = array(
'PurchaseOrder' => array(
'className' => 'PurchaseOrder',
'foreignKey' => 'foreign_key',
'counterCache' => array(
'attachment_count' => array('Pdf.model' => 'PurchaseOrder'),
)
),
My problem is when I try to use $this->PurchaseOrder->Zip->save($data); I run into problem because the alias Pdf is not found.
How do I overcome this while maintaining the countercache behavior of updating the attachment_count inside purchase_orders?
Note that if a PurchaseOrder is associated with 3 Pdf Attachments and 2 Zip Attachments, the attachment_count should read 3.
I am using cakephp 2.4.2
I stopped using counterCache and used afterSave instead.
/**
* Called after each successful save operation.
*
* #param boolean $created True if this save created a new record
* #param array $options Options passed from Model::save().
* #return void
* #link http://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave
* #see Model::save()
*/
public function afterSave($created, $options = array()) {
// update the PurchaseOrder based on pdf
if (isset($this->data['Pdf'])) {
$this->updatePurchaseOrderAttachmentCount($this->data);
}
}
/**
*
* #param Array $data where we expect key Pdf
*/
public function updatePurchaseOrderAttachmentCount($data) {
// check for Pdf
$pdfSet = isset($data['Pdf']);
if (!$pdfSet) {
throw new Exception('we expect Pdf as a key in your $data');
}
// check for foreign_key and model
$pdfFKSet = isset($data['Pdf']['foreign_key']);
$pdfModelSet = isset($data['Pdf']['model']);
if (!$pdfFKSet || !$pdfModelSet) {
throw new Exception('we expect foreign_key and model as keys in your $data["Pdf"]');
}
$count = $this->find('count', array(
'conditions' => array(
'model' => 'PurchaseOrder',
'type' => 'application/pdf',
'foreign_key' => $data['Pdf']['foreign_key'],
)
));
if (!is_numeric($count)) {
throw new Exception('we expect numeric in the $count');
}
$poData = array(
'PurchaseOrder' => array(
'id' => $data['Pdf']['foreign_key'],
'attachment_count' => $count,
)
);
return $this->PurchaseOrder->save($poData);
}
I know this post is very old, but I recently faced a similar issue and found another way to handle the same problem. You can continue using the counterCache approach with multiple counterCache, the only difference would be, you need to add you association on the fly, in the __construct method of your model.
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
/**
* When using Aliases, associations need to be added on the fly, otherwise, the CounterScope conditions would result in an SQL Error, during counterCache updates
*/
$this->bindModel(
array('belongsTo' => array(
'PurchaseOrder' => array(
'className' => 'PurchaseOrder',
'foreignKey' => 'foreign_key',
'counterCache' => array(
'attachment_count' => array($this->alias . '.model' => 'PurchaseOrder'),
'attachment_count' => array($this->alias . '.model' => 'PurchaseOrder')
)
)
)
)
);
}
I have 3 models (User, Message and Tag) with the following relations:
User hasMany Message
Message belongsto User
Message HABTM Tag
Tag HABTM Message
If a User is logged in he might want to see all Message tagged with something.
$messages = $this->Message->find('all', array(
'conditions' => array("Message.user_id" => $this->uid),
'contain' => array(
'Tag' => array(
'conditions' => array(
'Tag.id' => $activetag['Tag']['id']
)
)
));
However, this find will return ALL messages of that user. (Containable behaviour is included in both models)
Containable on child (tag) does not perform filter on the parent (message), that's why all the messages are returned. The containable only place condition on the tag itself, in your case, messages not matching $activeTag would still get returned but with empty tag array attached, while messages matching would return with an array containing only one tag, the $activeTag, but all messages would get returned.
For your purpose CakepHP recommend using join function for filtering with HABTM, it joins hasOne or belongsTo for you automatically but when it comes to HABTM you may need to perform the join yourself if needed.
assuming tables are named conventionally:
$this->Message->recursive = -1;
$options['joins'] = array(
array('table' => 'messages_tags',
'alias' => 'MessageTag',
'type' => 'INNER',
'conditions' => array(
'Message.id = MessageTag.message_id',
)
) );
$options['conditions'] = array(
'MessageTag.tag_id' => $activetag['Tag']['id'],
'Message.user_id' => $this->uid );
$message = $this->Message->find('all', $options);
more info here:
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables
In your Model Message add
**
* #see Model::$actsAs
*/
public $actsAs = array(
'Containable',
);
/**
* #see Model::$belongsTo
*/
public $belongsTo = array(
'Message' => array(
'className' => 'Message',
'foreignKey' => 'message_id',
),
'Tags' => array(
'className' => 'Tag',
'foreignKey' => 'tag_id',
),
);
in your controller :
// $tagsId = tags ids
$message = $this->MessageTag->find('all', array('conditions' => array('MessageTag.tag_id' => $tagsId),'contain' => array('Message')));
also is better follow cake naming convention, if you have tags(plural), message_tags(first singular second plural),messages(plural) tables you must have Tag,MessageTag,Message Models.
I'm trying to use on the fly associations to trim down the data I retrieve, but the model I'm using is associated to other models with a re-named field because I have 2 of the same models associated with it.
So, here's the model, say 'test', that has two 'user' fields, both related to the User model.
In the model:
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
),
'User_Watched' => array(
'className' => 'User',
'foreignKey' => 'user_id_watched'
)
);
When I retrieve data related to 'test', I want to only retrieve particular data linked to the 'User' and 'User_Watched' fields without any other nested information.
But when I do:
$this->User->unbindModel(array('hasMany' => array('something1', 'something2')), false);
then something1 and something2 data does not show up for the 'User' field of model 'test', but is still retrieved for the 'User_watched' field.
Can I not retrieve unwanted data for the 'User_watched' field?
Hope this makes sense... :)
KcYxA,
Containable behavior might help a lot in this case, as benjamin mentioned, your "find" queries would look like:
$this->User->find('first', array(
'conditions' => array('User.id' => $id),
'contain' => array('UserWatched')
));
In this case, you won't have to use unbindModel method. In this example, you'll get User and UserWatched data.
If you need only User data from "find", then tell Cake to "$this->User->contain();" so it won't go further then User model.
to use on the fly associations to trim
down the data I retrieve
Good idea.
'foreignKey' => 'user_id_watched'
should possibly be:
'foreignKey' => 'user_watched_id'.
Edit 1: At least this would make sense according to my current understanding. If user_id is a correct foreign key(FK), which cakephp uses to unbind the relations, but user_id_watched isn't, than your described behavior is explained.
Edit 2: The Containable behavior gives you another tool for controlling associated models.
Change $primaryKey in fly, run controller
Sample:
// Models
//....
class PreProductoDescripcion extends AppModel {
/**
* Primary key field
*
* #var string
*/
public $primaryKey = 'id_producto_descripcion';
//....
//....
}
class SenasaPedidosDetalles extends AppModel {
/**
* Display field
*
* #var string
*/
public $displayField = 'cod_tango';
public $belongsTo = array(
'SenasaPedidos' => array(
'className' => 'SenasaPedidos',
'foreignKey' => 'senasa_pedidos_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'PreProductoDescripcion' => array(
'className' => 'PreProductoDescripcion',
'foreignKey' => 'cod_tango',
//'conditions' => array('SenasaPedidosDetalles.cod_tango' => 'PreProductoDescripcion.codigo'),
'fields' => '',
'order' => ''
)
);
//....
#
// Controller Fly
//...
$this->SenasaPedidos->Behaviors->load('Containable');
$this->SenasaPedidos->SenasaPedidosDetalles->PreProductoDescripcion->primaryKey = 'codigo';
$datos = $this->SenasaPedidos->find(
'first', array(
'fields' => array( 'SenasaPedidos.*' ),
'conditions' => array( 'SenasaPedidos.id' => $id ),
'contain' => array(
'Usuarios' => array(
'fields' => array( 'Usuarios.apellido_nombre' )
),
'Clientes' => array(
'fields' => array( 'Clientes.razon_social' )
),
'Provincias' => array(
'fields' => array( 'Provincias.nombre' )
),
'Transportes' => array(
'fields' => array( 'Transportes.razon_social' )
),
'SenasaPedidosDetalles' => array(
'fields' => array( 'SenasaPedidosDetalles.*' ),
'PreProductoDescripcion' => array(
'fields' => array(
'PreProductoDescripcion.id_producto_descripcion',
'PreProductoDescripcion.descripcion'
)
)
),
)
));
//...