Why is saveAll() only saving the last record? - cakephp

I have a situation where, for external reasons, I have to directly save join records rather than saving them as part of a join. Here's what I mean:
I have a Firm model whose data is pulled from an external source.
I have a County model in my app database
I have a counties_firms join table that I use to associate those external firms to counties.
Because of what lives where, I'm not editing a Firm model nor am I editing a County model. I'm really just editing the associations. I have a Firm model to encapsulate anything I need to do with Firm data and one of these methods is Firm::saveCounties( $data ). It:
Accepts incoming data that includes the firm id and the county ids that should be associated.
Deletes all existing join records for that county
Attempts to save all of the new join records.
What I'm finding is that only the last county record is saved. Here's the incoming data:
Array
(
[0] => Array
(
[firm_id] => 13
[county_id] => 4fa16e24-a25c-4523-8a9e-7d1d147402e8
)
[1] => Array
(
[firm_id] => 13
[county_id] => 4fa16e27-ccd0-4f22-97da-7d1d147402e8
)
[2] => Array
(
[firm_id] => 13
[county_id] => 4fa16e4a-68f8-4fb1-95bb-7d1d147402e8
)
)
Given that data, I'm creating an on-the-fly association between Firm and CountiesFirm and attempting to $this->CountiesFirm->saveAll( $data ).
As I mentioned, only the last of the 3 county associations in this example is getting saved. Any idea what I might be missing?
Thanks.

Your array is fine. It seems the model is not clearing its id, you can try and add ['id'] => null to every record in your array to force model id clearing.
This worked for me.

I believe you are missing a level in your array... it should look something more like this...
Array(
'CountiesFirm' => array(
[0] => Array
(
[firm_id] => 13
[county_id] => 4fa16e24-a25c-4523-8a9e-7d1d147402e8
)
[1] => Array
(
[firm_id] => 13
[county_id] => 4fa16e27-ccd0-4f22-97da-7d1d147402e8
)
[2] => Array
(
[firm_id] => 13
[county_id] => 4fa16e4a-68f8-4fb1-95bb-7d1d147402e8
)
)
)
Try that and let me know you results

Related

Exclude empty results for HABTM relationship in CakePHP

Most of the CakePHP documentation seems to tell you how to filter based on a concrete relationship result. What I cannot seem to find is how to filter out results which have a relationship that returns no data.
For example, take the typical blog example which has Posts and Tags. Tags has and belongs to many Posts (HABTM). For this discussion, assume the following table structure:
posts ( id, title )
tags ( id, name )
posts_tags ( post_id, tag_id )
How do you find only the Tags which have one or more Posts associated with them (i.e. exclude Tags which would return no Posts)?
The ideal result set would look something like (quotes added for formatting):
Array (
[0] => Array (
[Tag] => Array (
[id] => 1
[name] => 'Tag1' )
[Post] => Array (
[0] => Array (
[id] => 1
[title] => 'Post1' )
[1] => Array (
[id] => 4
[title] => 'Post4' ) )
)
[1] => Array (
[Tag] => Array (
[id] => 4
[name] => 'Tag5' )
[Post] => Array (
[0] => Array (
[id] => 4
[title] => 'Post4' )
[1] => Array (
[id] => 5
[title] => 'Post5' )
[2] => Array (
[id] => 6
[title] => 'Post6' ) )
) )
The only way I've ever found to do this in a reliable way is to use ad hoc joins. Using these, you can specify an inner join type and get exactly what you want.
The following was tested with Cake 1.3.
To start you probably want to, or already do, have the HABTM relationship defined on the models for all the other circumstances where this normally applies:
class Post extends AppModel {
var $hasAndBelongsToMany = 'Tag';
}
class Tag extends AppModel {
var $hasAndBelongsToMany = 'Post';
}
According to Cake's own documentation:[1]
In CakePHP some associations (belongsTo and hasOne) perform 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. Here is
where forcing joins comes to the rescue. You only have to define the necessary joins
to combine tables and get the desired results for your query.
Excluding empty HABTM results is one of these times. This same section of the Cake Book explains how to accomplish this, but I didn't find it overly obvious from reading the text that the result achieves this. In the example in the Cake Book, they use the\ join path Book -> BooksTag -> Tags, instead of our Tag -> PostsTag -> Posts. For our example, we'd set it up as follows from in the TagController:
$options['joins'] = array(
array(
'table' => 'posts_tags',
'alias' => 'PostsTag',
'type' => 'INNER',
'foreignKey' => false,
'conditions' => 'PostsTag.tag_id = Tag.id'
),
array(
'table' => 'posts',
'alias' => 'Post',
'type' => 'INNER',
'foreignKey' => false,
'conditions' => 'Post.id = PostsTag.post_id'
)
);
$tagsWithPosts = $this->Tag->find('all', $options);
Make sure to set the foreignKey to false. This tells Cake that it should not attempt to figure out the join condition and instead use only the condition we supplied.
It's common that this will bring back duplicate rows due to the nature of the joins. To reduce the SQL returned use a DISTINCT on the fields as necessary. If you want all fields as is normally returned by find('all'), this adds the complication that you need to hard code each column. (Sure your table structure shouldn't change that often, but it could happen, or if you may just have a lot of columns). To grab all columns programmatically, add the following before the find method call:
$options['fields'] = array('DISTINCT Tag.'
. implode(', Tag.', array_keys($this->Tag->_schema)));
// **See note
It is important to note that the HABTM relationship runs AFTER the main select. Essentially, Cake gets the list of eligible Tags and then runs another round of SELECT statement(s) to get the associated Posts; you can see this from the SQL dump. The 'joins' we manually setup apply to the first select giving us the desired set of Tags. Then the built-in HABTM will run again to give us ALL associated Posts to those tags. We won't have any tags which have no Posts, our goal, but we may get posts associated with the tag that are not part of any of our initial 'conditions', if they were added.
For example, adding the following condition:
$options['conditions'] = 'Post.id = 1';
Will yield the following result:
Array (
[0] => Array (
[Tag] => Array (
[id] => 1
[name] => 'Tag1' )
[Post] => Array (
[0] => Array (
[id] => 1
[title] => 'Post1' )
[1] => Array (
[id] => 4
[title] => 'Post4' ) )
)
)
Based on the sample data in the question, only Tag1 was associated with our 'conditions' statement. So this was the only result returned by the 'joins'. However, since the HABTM ran after this, it grabbed all Posts (Post1 and Post4) that were associated with Tag1.
This method of using explicit joins to get the desired initial data set is also discussed in Quick Tip - Doing Ad-hoc Joins in Model::find(). This article also shows how to generalize the technique and add it to the AppModel extending find().
If we really only wanted to see Post1 as well, we would need to add a 'contain'[2] option clause:
$this->Tag->Behaviors->attach('Containable');
$options['contain'] = 'Post.id = 1';
Giving the result:
Array (
[0] => Array (
[Tag] => Array (
[id] => 1
[name] => 'Tag1' )
[Post] => Array (
[0] => Array (
[id] => 1
[title] => 'Post1' ) )
)
)
Instead of using Containable you could use bindModel to redefine the HABTM relationship with this instance of find(). In the bindModel you would add the desired Post condition:
$this->Tag->bindModel(array(
'hasAndBelongsToMany' => array(
'Post' => array('conditions' => 'Post.id = 1'))
)
);
I feel that for beginners trying to wrap their head around the automagic abilities of cake, making the explicit joins is easier to see and understand (I know it was for me). Another valid and arguably more 'Cake' way to do this would be to use unbindModel and bindModel exclusively. Teknoid over at http://nuts-and-bolts-of-cakephp.com has a good write up on how to do this: http://nuts-and-bolts-of-cakephp.com/2008/07/17/forcing-an-sql-join-in-cakephp/. Additionally, teknoid made this into a Behavior which you can grab from github: http://nuts-and-bolts-of-cakephp.com/2009/09/26/habtamable-behavior/
** This will pull the columns in the order defined in the database. So if the primary key is not defined first it may not apply DISTINCT as expected. You may need to modify this to use array_diff_key to filter out the primary key from $this->Model->primaryKey.

CakePHP update many to many resource

I have a category-product many to many relationship. When a product is created one or more categories are selected and then the product is saved. This works fine and the join table is correctly populated. The issue is when I go to edit the product, I can add more categories, change them etc... The issue comes when I try to save, it fails.
The line that I'm using to save is:
$this->Product->saveAll($this->data)
$this->Product->id is correctly populated and the debug of $this->data gives me an array like so :
Array(
[Product] => Array
(
[0] => 17
)
[Category] => Array
(
[0] => Array
(
[0] => 85
)
[1] => Array
(
[0] => 96
)
)
)
I don't know why it doesn't save as I can't find any detail on the error anywhere.
Any help much appreciated.
I think is this structure is wrong. If you are editing, where's the 'id' field of the product?
[Product] => Array
(
[id] => 17
)
I do that in an application. I will check it out tomorrow(today I cant view the source), but i think the id is the problem.
As raultm says, the structure was slightly wrong but it turned out to be validation in the model. The name and description fields were set to not allow empty and in the structure I wasn't passing these in (wasn't aware you had to on an update). By adding in the missing fields and naming the fields correctly in the array i.e. id, name and description, this worked.

What is the difference between save and saveAll function in cakephp?

can any one give example please
save is used to simply save a model:
Array
(
[ModelName] => Array
(
[fieldname1] => 'value'
[fieldname2] => 'value'
)
)
Assuming the above information was stored in an array called $data, one would call
$this->ModelName->save($data);
in order to INSERT a record into the model's table (if id field is not specified) or UPDATE a record of the model's table (if id field is specified).
saveAll is used to:
Save multiple records of a model
Array
(
[Article] => Array
(
[0] => Array
(
[title] => title 1
)
[1] => Array
(
[title] => title 2
)
)
)
So, you may save many models at the same time instead of looping and using save() each time.
Save related records of a model
Array
(
[User] => Array
(
[username] => billy
)
[Profile] => Array
(
[sex] => Male
[occupation] => Programmer
)
)
This would save both User and Profile models at the same time. Otherwise, you would have to call save() for User first, obtain the id of the newly saved user and then save Profile with user_id set to the obtained id.
Examples taken straight from the book.
saveAll saves all model data in a form, whereas save only saves one. So you would use save to save a single value, while saveAll basically saves you the trouble of using a loop for save.
As of Cake 2.0
save Saves model data (based on white-list, if supplied) to the
database. By default, validation occurs before save.
saveAll Saves multiple individual records for a single model; Also works with a single record, as well as all its associated records.

CakePHP save two models, second based on first

I have the following association:
Group hasAndBelongsToMany User, with the groups_users join table.
When I create a new group, I want to add to the join table the founder of the group.
My $this->data array looks like:
Array
(
[Group] => Array
(
[name] => seyufgsduifsj
[access] => 2
[founder_id] => 3
[random_key] => I6XC7uMTelpTSdq8DbtLPjqubiF7s6Pn
)
[GroupsUser] => Array
(
[user_id] => 3
[group_role_id] => 1
[random_key] => PZsDZXcoCTHw1IuvqsfURVpPX6AcZ3r2
)
)
I tried with save() and saveAll(), but it would add only the group in the groups table and not the user-group association.
So, basically, I have an add form where the user fills in the group name and access. Then, if the data validates, I add a couple of more field values to the group array (like random_key) and to the join table array (like user_id and group_role_id). I want Cake to save the group first, take its id, update the join table array with the proper group_id, and then save this second array too.
Is this possible in a straight-forward way, or do I have to write two consequent save() methods, for the second one providing the last inserted id in the first one?
Thanks!
You can use saveAll() function, but you need to change the format of the GroupsUser array. According to documentation of saveAll() your data should look like this:
Array
(
[Group] => Array
(
[name] => seyufgsduifsj
[access] => 2
[founder_id] => 3
[random_key] => I6XC7uMTelpTSdq8DbtLPjqubiF7s6Pn
)
[GroupsUser] => Array
(
[0] => Array
(
[user_id] => 3
[group_role_id] => 1
[random_key] => PZsDZXcoCTHw1IuvqsfURVpPX6AcZ3r2
)
)
)
Your example would work if the relation is Group belongsTo GroupsUser, but I believe this is not your case
Perhaps the related models are not passing validation. Use the following piece of code to save only if all models validate:
$this->Group->saveAll($this->data, array('validate' => 'only'));
If you don't find your Group being saved this time around, then it's most probably validation rules in your Join Model that are failing. My guess is that you have a notEmpty validation for the group_id, which you would have to remove in order for saveAll() to work.
Hope this points you in the direction of solving your problem.
EDIT: Your associations are a little off. Here's what it should be:
Group hasMany GroupsUser
User hasMany GroupsUser
GroupRole hasMany GroupsUser
GroupsUser belongsTo Group
GroupsUser belongsTo User
GroupsUser belongsTo GroupRole
Remove the hasAndBelongsToMany association between Groups and User. It doesn't exist any more.

CakePHP array structure from a HABTM query

I have two tables linked via a HABTM. countries & networks. They are linked together by the countries_networks lookup table.
I need to get all of the countries and all of the associated network ids for each corresponding country. I don't need to go as far as getting the network names, simply getting the ids from the lookup table will suffice.
If I do a find(all) it gives me an array of countries but not in the structure that I need. I need to return something link this, but I only need Country.countryName and CountriesNetwork.network_id:
Array
(
[0] => Array
(
[Country] => Array
(
[countryName] => Aeroplane
)
[Network] => Array
(
[0] => Array
(
[id] => 1
[CountriesNetwork] => Array
(
[id] => 1
[country_id] => 1
[network_id] => 1
)
)
[1] => Array
(
[id] => 7
[CountriesNetwork] => Array
(
[id] => 2
[country_id] => 1
[network_id] => 7
)
)
)
)
)
Is there a way of doing this with a findall()? as if I pass fields as an array I always seem to get an unknown column names SQL error.
Or even a custom query? Many thanks in advance to anyone who may be able to help.
Pickledegg,
Sounds like you should investigate the Containable behavior. It will allow you to specify conditions on the joined models like you want, and will return the data formatted in the same manner as the normal find() call.
Check the manual for more information.
Since you have no model for network_countries it's not possible to get those values using find or findAll. I think the simpliest solution will be to run a custom query to retrieve the data you need.
$query = "SELECT country_id, network_id
FROM countries C LEFT JOIN network_countries NC ON (NC.country_id = C.id)"
$this->Country->query($query);
Unfortunately the result returned using that code will be formatted slightly different then find results.
Or you could add a model for network countries but I don't think this is a good idea.

Resources