I have set up a hasMany through association between two tables - Books and Authors. The associations work properly, except when attempting to retrieve all the authors that belongs to a book. If I do this
$book = $this->Book->findById($id)
The array returned will not have an array Authors; it will have the AuthorToBook join model information, but it won't automatically fetch the Author associated with it.
I have to set recursive to 2 in order to retrieve the join model, and the associated Author. I am also re-fetching the Book data for each AuthorToBook association there is.
'AuthorToBook' => array(
(int) 0 => array(
'id' => '1',
'author_id' => '1',
'book_id' => '2',
'Author' => array(
'id' => '1',
'name' => 'J.R.R Tolkien',
'date_of_birth' => '1892-01-03 00:00:00',
'bio' => 'Professor and inventor of the Elvish Language'
),
'Book' => array(
'id' => '2',
'title' => 'Return of the King',
'pagecount' => '1200',
'publisher_id' => '1'
)
)
)
Now the question is - how can I fetch the associated model formed with a hasMany Through relationship without setting the recursive parameter?
Here's the source
<?php
class Author extends AppModel
{
public $hasMany = array('AuthorToBook');
}
?>
<?php
class AuthorToBook extends AppModel
{
public $useTable = 'authors_to_books';
public $belongsTo = array('Author', 'Book');
}
?>
<?php
class Book extends AppModel {
public $hasMany = array('AuthorToBook');
}
?>
Containable Behavior is your friend and is THE replacement for recursive. It allows you to specify exactly which associated model(s) you want to retrieve with each query.
Recursive is a cool thing, but it's pretty much agreed that you shouldn't actually use it beyond doing the intro tutorial for CakePHP. Set public $recursive = -1; in your AppModel (which makes it off by default), and never look back.
Related
Problem Synopsis:
(CakePHP v2.4.2) When I use saveAssociated (or saveAll, same result) for input for a new record with a hasMany/belongsTo relationship with multiple child elements, only the last child element gets saved because it INSERTs the first element, but then executes UPDATES for subsequent elements.
I've used saveAssociated for very similar purposes in this same application and had no problem with it, so I'm baffled.
Queries on all these work just fine, i.e., I get the multiple children associated with each parent.
Models synopsis:
class Site extends AppModel {
// sites columns: id (primary key), bunch of others
public $hasMany = array(
'SiteUser' => array(
'className' => 'SiteUser',
'foreignKey' => 'id', // Yes, I would have preferred 'site_id', lost battle
'dependent' => true
)
);
}
class SiteUser extends AppModel {
// site_users columns: rowid(PK), id (FK to sites), name
public $belongsTo = array(
'className' => 'Site',
'foreignKey' => 'id'
);
}
Equivalent request data (processed from form):
$site_data = array(
'Site' => array('field1' => 'value1', 'field2' => 'value2' ),
'SiteUser' => array(
array('name' => 'Jane Doe'),
array('name' => 'John Doe'),
array('name' => 'Moe Money')
)
);
In the controller:
unset($this->Site->SiteUser->validate['id']);
$saved_site = $this->Site->saveAssociated($site_data);
Results:
All of the Site data gets saved as expected. Only the last SiteUser element (Moe Money in the example) is saved. This is the same regardless of the number of elements in SiteUser, i.e., only the last element gets saved.
SQL Log:
It performs an
INSERT INTO site_users (`name`, `id`) VALUES ('Jane Doe', 1)
but then executes
UPDATE site_users SET 'name' = 'John Doe', 'id' = 1 WHERE site_users = 1
UPDATE site_users SET 'name' = 'Moe Money', 'id' = 1 WHERE site_users = 1
This obviously leaves the very last element as the one to get saved, the others are over-written by updates.
Thanks for any pointers in advance.
You better stick to the conventions, id as the foreign key? No, really, don't do that!
In any case you must tell your SiteUser model about the primary key column name in case it doesn't follow the conventions. See Cookbook > Models > Model Attributes > primaryKey
class SiteUser extends AppModel {
public $primarykey = 'rowid';
// ...
}
And while setting this to rowid will most likely fix the problem, I'd again advise to stick to the naming conventions instead!
I'm trying to do one of two things, use a virtual field from a model as the display field in my join model, or use the virtual field as the display in a find('list') on my join model.
Here's the current layout:
MODELS
<?php
class Participant extends AppModel {
public $hasMany = array('Registration');
public $virtualFields = array(
'full_name' => 'CONCAT(last_name, ", ", first_name)'
);
public $displayField = 'full_name';
}
-----
class Contest extends AppModel {
public $hasMany = array('Registration');
}
-----
class Registration extends AppModel {
public $belongsTo = array('Participant', 'Contest');
public $hasMany = array('Assignment');
}
?>
Tables are as follows:
participants contests registrations
------------ --------- --------------
id id id
first_name name contest_id
last_name participant_id
In my contests controller I'm trying to develop a list to be viewed as checkboxes in the view.
Here is the excerpt from my contests controller:
$this->loadModel('Registration');
$this->set('registrations', $this->Registration->find(
'list',
array(
'conditions' => array('contest_id' => $contestId),
'recursive' => 1
)
)
);
//$contestId is defined previously, and I have verified such.
This all actually runs fine as it is, and in the view will display a column of checkboxes with the registration_id as the label next to the checkbox.
I would like to get a full_name as is defined in the Participant model to be the displayField of the Registration model. I've been searching and can't quite seem to find a good way of doing that. I hope I have been descriptive enough, and please let me know if you have any questions and I'll try to explain better. Thank you.
edit: I'm using CakePHP 2.4.
try to add the 'fields' parameter
$this->loadModel('Registration');
$this->set('registrations', $this->Registration->find(
'list',
array(
'fields' => array('Registration.id', 'Participant.full_name'),
'conditions' => array('contest_id' => $contestId),
'recursive' => 1
)
));
edit: apparently cake does not place virtual fields of associated models when using find() with 'fields' options.
So you have to build your array by yourself, hoping your models use the containable behavior
$registrations = $this->Registration->find(
'all',
array(
'contain' => array(
'Participant' => array('full_name')
),
'conditions' => array('contest_id' => $contestId),
)
);
$registrations = Hash::combine($registrations, '{n}.Registration.id', '{n}.Participant.full_name');
$this->set('registrations', $registrations);
I have two models with a one-to-one relationship, say a Person and a Hat. The foreign key is in Person so Person belongsTo Hat and Hat hasOne Person.
Now in the create form for Person I can just use a custom find() to find all Hats that are not taken already and the automagic will populate a combobox with all available Hats for me.
But how do I design the edit form for a Person? Here I would like a combobox with all available Hats plus the currently chosen Hat (if not null), with the current Hat as default choice.
I haven't worked with Cake for a while so I might be missing something obvious, or is the problem in the design?
Person Model:
class Person extends AppModel
{
public $name = 'Person';
public $belongsTo = array('Hat' => array('className' => 'Hat',
'foreignKey' => 'hat_id'));
}
Hat Model:
class Hat extends AppModel
{
public $name = 'Hat';
public $hasOne = array('Person' => array('className' => 'Person',
'foreignKey' => 'hat_id'));
}
All Available hats that are not assigned to anyone:
$avaiable_hats = $this->Hat->find('all', array('conditions' => array('Person.hat_id <> ' => 'Hat.id')));
All assigned hats
$assigned_hats = $this->Person->find('all', array('conditions' => array('Person.hat_id IS NOT NULL',
'Person.hat_id <> 0')));
If you want to make a combobox list, use the following:
All Available hats options that are not assigned to anyone
$available_hats_options = $this->Hat->find('list', array('conditions' => array('Person.hat_id <> ' => 'Hat.id'),
'fields' => array('Hat.id', 'Hat.hat_name')));
And you can make the dropdown list in your view using the following syntax:
$this->Form->input('available_hats', array('options' => $available_hats_options, 'div' => false, 'label' => 'Available Hats'));
I have two tables Contact and Quote, this is a one to many relationship (i.e. one contact can have many quotes). Foreign keys are all setup correctly.
When I go to create a new quote I want to be able to select from a drop down list of contacts.
My code looks like this:
Contact Model:
class Contact extends AppModel {
public $hasMany = array('Quote' => array('className' => 'Quote', 'foreignKey' => 'contact_id'));
}
Quote Model
class Quote extends AppModel {
public $belongsTo = array('Contact' => array('className' => 'Contact', 'foreignKey' => 'contact_id'));
public $validate = array(
'name' => array(
'rule' => 'notEmpty'
),
'amount' => array(
'rule' => 'notEmpty'
)
);
}
Add method in QuotesController:
public function add() {
// TODO: Update this so the user can select the id from a drop down list.
$this->request->data['Quote']['contact_id'] = '1';
if ($this->request->is('post')) {
$this->Quote->create(); // This line writes the details to the database.
if ($this->Quote->save($this->request->data)) {
$this->Session->setFlash('Your quote has been saved.');
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('Unable to add your quote.');
}
}
}
As you can see I'm currently just hard coding the user id as part of the add process.
I'm assuming you have read and are using this method in your view.
http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::select
Less easy to find (or easier to overlook) is this:
http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#find-list
So in your controller you want to do
$contacts = $this->Article->find('list', array('fields' => array('Contact.id', 'Contact.name'));
$this->set(compact('contacts'));
Then in the view:
echo $this->Form->select('contact_id', $contacts);
Modify the fields for the find to reflect what is actually in your model. And if you need fields combined, you can possibly do it with virtual fields: http://book.cakephp.org/2.0/en/models/virtual-fields.html. Otherwise you can select the fields that need to be composited and use a foreach loop to combine them into a id=>[displayed value] array to pass on to the view. Only the id is the important thing and has to correspond to an id in the Contacts table.
I have a 'patient_cases' table with a HABTM relationship with a 'procedures' table. This 'procedures' tables has a many to one relationship with 'consultants' table, which then has a many to one relationship with 'specialties' table.
The relationships are working fine and using cakephp debug tool bar i can see everything is coming through correctly when i look at 'patient_cases' index page, however i'm only getting consultant_id and specialty_id rather than all of the fields with that associated model.
which is what should be happening i believe -
In CakePHP some associations (belongsTo and hasOne) performs automatic
joins to retrieve data, so you can issue queries to retrieve models
based on data in the related one.
But this is not the case with hasMany and hasAndBelongsToMany
associations.
I want to be able to get the consultant name from 'consultants' table using procedures.consultant_id and get the specialty name from 'specialties' using consultant.specialty_id.
I have tried playing about with some table joins but with no success,
class PatientCase extends AppModel {
public $hasAndBelongsToMany = array(
'Procedure' => array(
'className' => 'Procedure',
'joinTable' => 'patient_cases_procedures',
'foreignKey' => 'patient_case_id',
'associationForeignKey' => 'procedure_id',
'unique' => true
)
);
}
class Procedure extends AppModel {
public $belongsTo = array(
'Consultant' => array(
'className' => 'Consultant'
)
);
}
class Consultant extends AppModel {
public $belongsTo = array(
'Specialty' => array(
'className' => 'Specialty',
'conditions' => array('Specialty.active' => 1)
)
);
public $hasMany = array(
'Procedure'
);
}
class Specialty extends AppModel {
public $hasOne = array(
'Consultant'
);
);
}
$this->set('patientCases', $this->PatientCase->find('all'));
Returns everything i need, each model array is returned (other relationships not mentioned here), however the model array 'Consultant' is not returned, which also means i can not get the 'Specialty' data which is linked to the 'Consultant' model
EDIT
I have managed to get the data i need by using this in my PatientCasesController
$this->set('patientCases', $this->PatientCase->find('all'));
$this->set('patientCaseProcedures', $this->PatientCase->Procedure->find('all'));
However, ideally i want to return everything in the one 'patientCases', is this possible?
EDIT
I have solved the problem using the recursive function
$this->PatientCase->recursive = 3;
$this->set('patientCases', $this->PatientCase->find('all'));