CakePHP 2.1 - Saving (and creating) multiple Join Models and associated models - cakephp

My model relationship is as follows:
Student hasMany ClassStudent
Class hasMany ClassStudent
ClassStudent belongsTo Student
ClassStudent belongsTo Class
ClassStudent is a join model.
What I want to do is create Student(s), use an existing Class or create a new one, and create a join model record that links the Students and classes.
I want to do this all in one call to save (if this is even possible).
I have tried:
$data = array(
'Student' => array(
'0' => array( ... ), // Data in here
'1' => array( ... ),
...,
'n' => array( ... )
),
'Class' => array(
'class_id' => x // The class that I want the above students to be associated with
)
)
What I want to do is create n records of students and also add them to a class (possibly creating a class at the same time if the users wants to add a new one). I also want to create a join model record for each Student to the Class when I am creating the Student records.
Is this possible? I am using Cake 2.1.0 (today's stable release), and I have tried the different types of saveAll (saveAssociated and saveMany) with $options['deep'] = true.
Is it possible my data array is not in the correct format?
EDIT:
I have also tried:
$data = array(
'ClassStudent' => array(
'0' => array(
'Student' => array (...), // Data
'Class' => array(id => x) // The id of the Class the Student should be associated to
...,
'n' => array(
'Student' => array(...), // n-th Student
'Class' => array(id => x)
)
);
$this->saveAll($data['ClassStudent'], array('deep' => true));
In the above case, it successfully creates new Student records in the students table, but nothing is created in the join table.

saveAll (saveAll is a wrapper to saveMany and saveAssociated) is the right tool for the job. Take a look at the documentation for saveAll, I don't see any notes about it changing for 2.1. Having taking more time to read, here are some thoughts
First off, is there a type in your array structure. You have
'Class' => array ( 'class_id' => x )
If the Class is already defined and you are just wanting to add a student, then it would be
'ClassStudent' => array( 'class_id' => x )
With that said, CakesPHP ORM should allow you to use a saveAll with on a hasMany using a numerical index, so if assuming you have a typo, there follow might work for you
$data = array(
'Student' => array(
'0' => array( ... ), // Data in here
'1' => array( ... ),
...,
'n' => array( ... )
),
'ClassStudent' => array(
'class_id' => x // The class that I want the above students to be associated with
)
)
$this->ClassStudent->saveAll($data);

Related

Saving data with HABTM and HasMany looses foreignKey

I have a User model with two relations:
HasAndBelongsToMany
public $hasAndBelongsToMany = array(
'group' => array(
'className' => 'group',
'foreignKey' => 'user_id'
)
);
HasMany
public $hasMany = array(
'phonenumber' => array(
'className' => 'phonenumber',
'foreignKey' => 'user_id'
)
);
Phonenumber and Group have set
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
When I use
$this->saveAll(array(
'User' => $data,
'phonenumber' => $numbers, //array with data
'group' => $groups //array with data
)
);
The data gets saved in the tabels but User_id is "0" in phonenumber and group table.
How can I get the correct ID saved ? (CakePHP v 2.5)
FWIW saveAll() should work as advertised, populating the new user_id in the child tables in one fell swoop.
Have you paid attention to the relevant options: atomic & deep?
Especially if database does not support transactions, you'll need to pass in atomic:
$this->saveAll(array(
'User' => $data,
'phonenumber' => $numbers, //array with data
'group' => $groups //array with data
),
array('atomic' => false)
);
Considering the CakePHP documentation you will find this hint:
When working with associated models, it is important to realize that
saving model data should always be done by the corresponding CakePHP
model. If you are saving a new Post and its associated Comments, then
you would use both Post and Comment models during the save operation
(http://book.cakephp.org/2.0/en/models/saving-your-data.html#saving-related-model-data-hasone-hasmany-belongsto)
Based on that information I suggest you try the following:
$this->User->save($data); // first save the user
Assuming you have multiple numbers:
foreach($numbers as $key => $nbr) {
// set user_id related data
$numbers[ $key ]['Phonenumber']['user_id'] = $this->User->id;
}
Finally save your related data:
$this->User->Phonenumber->saveAll($numbers);
Since this code is untested you may need to take some adjustments. Always ensure to follow the Cake-Conventions and use CamelCase ModelNames.

CakePHP conditions in associated Models

I have a database with Classes, Semesters, Users, and Visits(Visits is a join table for Users and Classes)
The relations are:
User hasAndBelongsToMany Class (through Visits
Class belongsTo Semester
Now I want to view all Visits with Classes in an active Semester (The Semester table has a field is_active)
I read about a contain option for the find method and tried something like this:
$option = array(
"contain" => array(
"Class" => array(
"Semester" => array(
"conditions" => array("Semester.is_active" => true)
)
),
),
"conditions" => array(
"Visit.user_id" => $id,
)
);
But with this, classes in a not active semester are found, only the semester isn't.
Is something wrong with this? Or is there an other way?
Now I have a solution:
I used the joins option for the find method.
"joins" => array(
array(
"table" => "classes_semesters",
"alias" => "ClassesSemesters",
"type" => "inner",
"conditions" => array(
"Visit.class_id = ClassesSemesters.class_id"
)
),
array(
"table" => "semesters",
"alias" => "Semester",
"type" => "inner",
"conditions" => array(
"ClassesSemesters.semester_id = Semester.id"
)
)
),
"conditions" => array(
"Visit.user_id" => $id,
"Semester.is_active" => true
),
When you use contain, like in your case, the query is not a direct inner join, that is, Class inner joins with Semester.
the class records are pulled first, then there will be a second query on the semesters table with the condition (where class_id IN ( results of first query).
so even if there are no records found in the semesters table, cake will still return the Class records found.
query 1. $class = select * from classes where bla bla
query 2. select * from semesters where class_id in (result from query 1) and other conditions bla bla
cake then merges the result from the 2 queries together to produce 1 result.

CakePHP - paginate and sort 2nd level association

I understand that this has been asked 100,000 times but I've pretty much read all 100,000 replies and none of them seem to match what I'm after. I've tried every possible combination (obviously not) and I'm afraid I'm getting beat at something so relatively simple. This is my 2nd Cake project, so I'm by no means an expert.
Profile -> BelongsTo -> Store, Store -> BelongsTo -> Region, Region -> HasMany -> Stores
Profile.php
class Profile extends AppModel {
public $belongsTo = array(
'Store' => array(
'className' => 'Store',
'foreignKey' => 'store_id'....
Store.php
class Store extends AppModel {
public $belongsTo = array(
'Region' => array(
'className' => 'Region',
'foreignKey' => 'region_id'....
Region.php
class Region extends AppModel {
public $hasMany = array(
'Store' => array(
'className' => 'Store',
'foreignKey' => 'region_id'....
ProfileController.php
$this->Paginator->settings = array(
'conditions' => array('Profile.job_title_id' => '1'),
'contain' => array(
'Store'=> array(
'Region'
)
)
);
$UserArds = $this->Paginator->paginate('Profile');
$this->set(compact('UserArds'));
view.php
<th><?php echo $this->Paginator->sort('Region.name', 'Region'); ?></th>
All I'm looking to do, is to be able to sort by Region name when using paginator. No matter what combination I use, I just can't seem to be able to sort Region.name. The order By clause is omitted with all other 2-level-deep associations but works fine any other time (with same level or 1st level).
Can anyone suggest a fix for this simple mistake?
Look at the SQL debug output, the regions are being retrieved using separate queries, that's how Cakes ORM currently works.
In your case there's a relatively simple workaround. Just create a proper association on the fly, for example Profile belongsTo Region.
Here's an (untested) example, it adds a belongsTo association with the foreignKey option set to false in order to disable the automatic foreign key generation by the ORM, this is necessary as it would otherwise look for something like Profile.region_id. Note that consequently the contain config has to be changed too!
$this->Profile->bindModel(array(
'belongsTo' => array(
'Region' => array(
'foreignKey' => false,
'conditions' => array('Region.id = Store.region_id')
),
)
));
$this->Paginator->settings = array(
'conditions' => array('Profile.job_title_id' => '1'),
'contain' => array('Store', 'Region')
);
That should generate a proper query that includes the stores as well as the regions table, making it possible to sort on Region.name. Note that the resulting array will be a little different, Region will not be nested in Store, everything will be placed in the same level, ie:
Array
(
[Profile] => Array
(
....
)
[Store] => Array
(
....
)
[Region] => Array
(
....
)
)
Either you modify your view code accordingly, or you transform the retrieved data before passing it to the view. A third option would be to include a nested Region in the contain config, however that would result in additional queries that are totally unnecessary as the data was already fetched with the main query, so I wouldn't recommend that.

CakePHP Many To Many With Conditions

I'm newbie on CakePHP, and now I'm stuck on many to many situation
ok, i have 3 Table :
questions
with fields (id, question)
question_product
with fields (id, question_id, product_id, question_number, is_enabled)
products
with fields (id, name, code, is_enabled)
so when i want to select questions with specific field, i don't know how to fix it
for now, my code is like this :
Question.php (Model)
class Question extends AppModel {
public $hasAndBelongsToMany = array (
'Product' => array (
'joinTable' => 'question_product',
'foreignKey' => 'question_id',
'associationForeignKey' => 'product_id',
'unique' => 'keepExisting',
'order' => 'question_number',
'fields' => array (
'QuestionProduct.question_number',
'Product.id',
'Product.name'
),
'conditions' => array (
'QuestionProduct.is_enabled' => 1,
)
)
);
}
QuestionsController.php (Controller)
public function loadQuestions($productId) {
$this->view = 'load_questions';
$questions = $this->Question->find('all', array (
'fields' => array (
'Question.id',
'Question.question',
'Question.is_optional',
'Question.reason_optional',
'Question.text_size'
),
'conditions' => array (
'QuestionProduct.product_id' => $productId
)
));
$this->set($questions);
}
method loadQuestions have one parameter to select with specified product
if i using sql query, it will be like this
select all from Question with condition Product.product_id=4, sorted by QuestionProduct.question_number ascending
select questions.*
from questions
join question_product on questions.id=question_product.question_id
join products on products.id=question_product.product_id
where products.id=4
order by question_product.question_number;
any answer will be appreciated :)
Thanks !
Any time you use a many-many (HABTM) relation with any other field that requires conditions, it is no longer many-many as far as Cake is concerned. You want the HasManyThrough relationship
Instead of using hasAndBelongsToMany relation, use two belongsTO relation from question_product to questions and another time from question_product to products.
question_product belognsTo questions
question_product belongsTo products
NOTE:you should change the table name from question_product to question_products as cakePHP convention
in your model QuestionProduct model :
<?php
// declares a package for a class
App::uses('AppModel', 'Model');
class QuestionProduct extends AppModel {
/**
* #see Model::$actsAs
*/
public $actsAs = array(
'Containable',
);
/**
* #see Model::$belongsTo
*/
public $belongsTo = array(
'Product' => array(
'className' => 'Product',
'foreignKey' => 'product_id',
),
'Question' => array(
'className' => 'Question',
'foreignKey' => 'question_id',
),
);
then in your Controller :
public function loadQuestions($productId) {
$this->view = 'load_questions';
$questions = $this->QuestionProduct->find('all', array (
'fields' => array (
'Question.id',
'Question.question',
'Question.is_optional',
'Question.reason_optional',
'Question.text_size'
),
'conditions' => array (
'QuestionProduct.product_id' => $productId
),
'contain' => array('Product','Question')
));
$this->set($questions);
}
It should make exactly the query you want, and I don't think it has any other way to produce that query.

Saving multiple hasMany through at once

I have a many-to-many relation between 2 models Invoiceand Position defined with the hasMany through method like described in the Cookbook.
Now when i create a Invoice, i want to save multiple Positions to that Invoice in one save()-operation. Something like this:
$data = array(
'Invoice' => array(
// invoice stuff
),
'InvoicesPosition' => array(
[0] => array(
// additional meta information
'Position' => array(
// position stuff
// ...
),
),
[1] => array(
// ...
'Position' => array(
// position stuff
// ...
),
),
// and so on
),
);
$this->Invoice->saveAll($data, array('deep' => true));
The result should be one new Invoice, with 2 Positions linked to it
(That means 1 new record in the invoices table, 2 new records in the positions table and 2 new records in the join table).
Is it possible with Cake's built-in methods? Or do i have to overwrite the saveAll()-method for that model?
Use 'deep' => true option in saveAll() method.
$this->Invoice->saveAll($data, array('deep' => true));

Resources