Practical use of saveAll() (part 1, working with multiple models)

(Part 2 is here)

I would like to provide some insight on how to use the saveAll() method, with a few real-world examples. First, I’m going to cover how to use saveAll() when working with two related models.

Imagine we are building a CRM of some sort. We have a Company model, which holds some generic Company information. Company hasMany Account, which would hold the information about various users who can access the CRM.

Let’s go ahead and create our tables:

CREATE TABLE `companies` (
  `id` int(11) NOT NULL auto_increment PRIMARY KEY,
  `name` varchar(200) NOT NULL,
  `description` varchar(200) NOT NULL,
  `location` varchar(200) NOT NULL,
  `created` datetime NOT NULL
)
 CREATE TABLE `accounts` (
`id` INT NOT NULL auto_increment PRIMARY KEY,
`company_id` int(11) NOT NULL,
`name` VARCHAR( 200 ) NOT NULL,
`username` VARCHAR( 200 ) NOT NULL,
`email` VARCHAR( 200 ) NOT NULL,
`created` DATETIME NOT NULL
)

Then we can setup the Company and Account models as follows:

class Company extends AppModel {
	var $name = 'Company';	

	var $hasMany= array('Account');

	var $validate = array(
            'name' => array('rule' => array('notEmpty')),
            'description' => array('rule' => array('notEmpty'))
    );
}
class Account extends AppModel {
	var $name = 'Account';	

	var $belongsTo = array('Company');

	var $validate = array(
            'name' => array('rule' => 'notEmpty'),
            'username' => array('rule' => 'notEmpty'),
	    'email' => array('rule' => 'email')

    );
}

As you can see, the models are pretty simplistic, for example the Account model is missing an obvious password field, but I’m leaving it up to you to extend the models as you wish. At least we’ve added some validation and all in all that should suffice for the purposes of this example.
Please note, that I’m using a new (at the time of writing) validation rule ‘notEmpty’ if you do not have the latest version of cake (nightly build of 07/31/2008 or later), this rule may not be available in your core.

Next, let’s create a Companies controller, we’ll leave it empty for now:

class CompaniesController extends AppController {
 var $name = 'Companies';
}

Now that we have our models and a controller, our goal is to build a form where some CRM user would setup a company and a first default account at the same time. This is where saveAll() comes in very handy, because it allows us to save both models without any effort.

So, let’s build a form, which would allow us to create a Company and an Account (create a file called /companies/add.ctp):


echo $form->create();
echo $form->input('Company.name', array('label'=>'Company name'));
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('Account.0.name', array('label'=>'Account name'));
echo $form->input('Account.0.username');
echo $form->input('Account.0.email');

echo $form->end('Add');

Let’s take a look at what’s going on here. We consider Company to be our main model, therefore the form by default will post to Companies controller’s add action (i.e. /companies/add/).
Take a look at the way I named the form fields for the Account model. If Company is our main model saveAll() will expect the related model’s (Account) data to arrive in a specific format. And having Account.0.fieldName is exactly what we need (this is only true for hasMany relationship, for hasOne the fields would follow Account.fieldName format… I hope it makes sense as to why).
Having the label for the two fields allows us to be more descriptive, otherwise CakePHP would label both fields as just “Name” by default, which would be confusing to the user.

Now, in our Companies controller we can create an add() action:

function add() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));
   }
}

Is that easy or what?

A quick thing to point out here, is the use of array(‘validate’=>’first’), this option will ensure that both of our models are validated. You can refer to the API for other options that saveAll() accepts, but this is good enough for the current example (and most similar cases).

Now let’s take our tiny app for a test drive and try to submit the form with empty data. If all goes well, the validation should take place for both models and you should see our relevant fields being invalidated.

Go ahead, and try to save some data. Looking at your SQL debug, you’ll see that cake saved both models and established the correct relationship by saving company_id field with the correct id into our accounts table. Ah, the wonders of automagic…

Well, we are not done just yet. Let’s now build a form and a related action for editing our Company and Account.

For the purposes of our example let’s do something like this for the edit action:

function edit() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));
   }
   else {
      $this->Session->write('AccountId', 2);
      $this->data = $this->Company->Account->find('first', array(
						  'conditions'=>array(
						  	'Account.id'=> $this->Session->read('AccountId');
									)));
   }
}

We’ll imagine that at this point the user (or technically Account) is logged into our application and we’ve stored the Account.id into the session by using: $this->Session->write(‘AccountId’, 2);

In your case this id might be different, so please ensure that it’s a valid one by looking at your DB.

Of course in reality you would rely on CakePHP’s auth or some other log-in method, but that would be beyond the scope of this article.

Lastly let’s build our edit form:


echo $form->create('Company', array('controller'=>'companies',
								    'action'=>'edit'));

echo $form->input('Company.name');
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('Account.'.$session->read('AccountId').'.name', array('label'=>'Account name'));
echo $form->input('Account.'.$session->read('AccountId').'.username');
echo $form->input('Account.'.$session->read('AccountId').'.email');

echo $form->input ('Company.id');
echo $form->input('Account.'.$session->read('AccountId').'.id');

echo $form->end('Edit');

————————-

Update: In CakePHP 1.2 RC3+ you can name your edit form fields with any key (not necessarily the model ID).

Although using model ID for the key name still works, it is no longer necessary to correctly display the validation errors. The code above and text below refers to the older way of doing things, but it should still give you an idea of how to approach the edit form.
————————-

There are a couple of things to point out here, first you’ll notice that in this form I’ve named the Account model fields by using $session->read(‘AccountId’) as the array key. This is needed in order to properly display the validation errors.
If you are unsure about what I mean pr($this->validationErrors) in your view and pay attention to the value of the key, which holds the errors for the Account model. You’ll notice that it will be matching the id of the model we are currently editing.

If you think back to our add() action, the Account model had no data (and therefore no id) and we were just creating one and only account, therefore Account.0.fieldName would work just fine.

In the case of edit() we already know the id of the Account model, therefore the field names must match the actual id from the database. Of course, we know that in this example it was 2, but we do have to make our form dynamic, so by using $session->read(‘AccountId’) we ensure that our fields will always be named correctly regardless of whether the actual value of the id is 2 or 76.

Next thing to notice is that we’ve added two inputs, which hold the id’s of our models. This is needed to ensure that an UPDATE query is triggered, rather than an INSERT. And yes, cake is smart enough to make those inputs as hidden fields.

Well, if you’ve managed to follow all along, congratulations we are done. I hope that now you can see how saveAll() can make your life so much easier with so little effort.

38 thoughts on “Practical use of saveAll() (part 1, working with multiple models)

  1. Excelent article! This is another obscure topic in CakePHP, but this article uncovers most of it! Two question for you…

    1) Suppose I want to add more than 1 account in the add action, having inputs like Account.1.name, Account.2.name, etc. Does saveAll save all this accounts?

    2) Now suppose I wanna add a new account in the edit action. If I put a Account.0.name input, does saveAll saves this too?

    Thanks

  2. @Martin Bavio

    Thank you.

    1. Yes
    2. Yes

    In the next part I’m gong to cover saving multiple records… and probably will add multiple models + multiple records.

  3. Thanks teknoid.

    I referenced this post in a comment on the cookbook (section 3.7.4 Saving Your Data), as the most informative explanation available, on the use of SaveAll().

    Please keep up the great work.

    Your blog in general has become extremely helpful to many people.

  4. Is it possible to saveAll to the third level? e.g. Body->Foot->Shoe?

    I’ve tried unsuccessfully. Also, do you have a direct link to the official documentation? Cheers.

  5. @elliptik

    I have not tried saving deeper relationships and I have not seen a supporting test case for this (perhaps I missed it). This is something I was going to investigate and report on in one of the next posts.

    saveAll() is not really documented in the official manual…

  6. it might be worth mentioning – as I recall it being a source of confusion on IRC at one point – that HABTM relations don’t need the saveAll, just a save() – as they work differently.
    (e.g. your example if it were Post HABTM Tag, would use a save() to save the tags. )

    This is a great article, by the way – clear and concise, thank you – I am liking your blog too, monsieur teknoid!

  7. @phpcurious

    Thank you.

    Unfortunately that !empty issue is a problem with Word Press, the source code is correct, but for some reason after the style is applied it screws up the display.

  8. @luke

    Thank you.

    You are correct about save() + HABTM. Yet, there is an important difference…
    In this example saveAll() is used to create two brand new models at the same time, with HABTM you can create one model, but you need to know the id’s of another in order to establish the association in the DB.
    I’ve not found a way (or a test case) to create two HABTM models at the same time.

  9. teknoid – yes I really just wanted to add it as I remember ttrying to convince someone on IRC that saveAll was not for HABTM model relations, and they needed just save().

    The Flickr example of new tags being defined as you save your image would be great – I am sure it can be done in Cake, and Ajax can be useful anyway with these types things. Just off to read your 2nd tutorial!

  10. @primeminister

    Thanks, I do hope it can be used for the manual (at least some of it), would be a good addition :)

  11. Wow. This save my life :)

    But I have a question.

    Lets suppose that I have two models (ModelA and ModelB). ModelA hasMany ModelB.

    In edit form there is a way to create several inputs without using foreach?

    Data in the edit form:

    Array
    (
    [ModelA] => Array
    (
    [id] => 36
    [fieldname1] => aaaaaaa
    [fieldname2] => 00.000.000/0000-00
    [fieldname3] => 111111111
    )
    [ModelB] => Array
    (
    [0] => Array
    (
    [id] => 1
    [fieldname1] => (00)0000-0000
    [modelA_id] => 36
    )
    [1] => Array
    (
    [id] => 2
    [fieldname1] => (00)0000-0000
    [modelA_id] => 36
    )
    )
    )

  12. Just a thought. If you use company->saveAll as depicted above, the malicious user can easily use javascript to change the ‘hidden’ Account.n.id to whatever they like, and change other records.

    They must to be verified before saving, or you will eventually lose data.

  13. @DanW

    That’s true, but the purpose of this post was to demonstrate the usage of saveAll(), securing the application would be beyond the scope of this article and would add unnecessary complexity to the sample code.

  14. @DanW: How can a ‘malicious’ user do that? And how would you prevent this?
    My first idea was to check, if the user, who wants to edit, is the same user who added the data.
    But, the username could also be faked by a javascript then, or…?

  15. @volka

    There are some tools that will let one modify the form POST data…
    Check out another post on this blog about using the Security component to ensure that something like that doesn’t happen to you ;)

  16. Hi Teknoid,
    excellent article thank you. I am having an issue with saveAll() in my edit method which I am hoping you can help me with. For some reason that I cant determine it’s creating new records in both tables instead up updating them. I am sure it’s something goofy that I have missed.

    class Recipe extends AppModel
    {
    var $hasMany= array(‘IngredientList’);
    }

    class IngredientList extends AppModel
    {
    var $belongsTo = array(‘Recipe’);
    }

    function edit($id = null)
    {
    $this->Recipe->id = $id;
    $this->set_recipe_types();
    $this->set_ingredients();
    $this->set_measurement_types();

    $this->set(‘ingredient_list’, $this->IngredientList->find(‘all’, array (‘recipe_id’ => $id)));

    if (empty($this->data))
    {
    $this->data = $this->Recipe->read();
    }
    else
    {
    if ($this->Recipe->saveAll($this->data))
    {
    $this->flash(‘Your recipe has been updated.’,’/recipes’);
    }
    }
    }

    Thanks for any advice you can give.

  17. @Steven Wright

    Thanks.

    Are you sure that id is being passed as part of $this->data correctly? Do you have a hidden form field that allows cake to do so?
    Basically, if the model’s id (or primary key) is not set (or passed as part of the data array) that would be the only reason it would create a new record, rather than update an existing one.
    Looking at your code you set the id on the first page load, while you grab it from the URL, make sure that it is also set/passed when you do a form POST.

  18. only a little comment about a possible mistake:

    if you have a validation rule for a foreignKey in the hasMany model that requires the field not to be empty, it will fail the validation rules and You wont be able to save Your data with saveAll. As the foreignKey is probably not displayed on Your form, You will not notice the validation error and You will see only a message that data has not been saved but no further info. If this happens, then check validationErrors with debug and You may find some shaddy validation rules that are causing this.

  19. @zoltan

    Not sure about that, I’m pretty positive I’ve tested this scenario and all of the relevant validation rules should be displayed.

  20. Disclaimer: I am a newbie to CakePHP!

    In my experiments today, it seems that if saveAll will not update some associated records and insert others; ie, it is either updating or inserting, but it won’t do both. Why do I think this is worth mentioning? Well, it seemed a natural thing to want to do in my example:

    Profile hasMany Preference
    Preference belongsTo PreferenceType
    (ie, preferences table columns are id, profile_id, preference_type_id, value)

    Let’s say that on the Profile edit view, I let you update your name and age, and then there is a list of Preferences which I got from the preference_types table. Some of these Preferences are already set and therefore we want to edit them; others may be set new and therefore they don’t have Preference.x.id set (they only have Preference.x.value and Preference.x.preference_type_id).

    When I call saveAll, it is choking when it gets to the part of the array with the unset Preference id. It is also not returning false.

    If there is some clever way to do this with saveAll, I haven’t found it. Looping over the associated records and calling save() in the Preference model works OK though.

  21. Ah, nevermind to the majority of my comment. It will do the insert… it was a validation problem, though I’m pretty sure it isn’t returning false as I expected, so I have to dig into that further and see what I’m doing wrong.

  22. @Amanda Allen

    I’m glad you’ve got part of it sorted out…

    Just a quick reminder, for saveAll() to work correctly you need to have a DB that supports transactions (i.e. InnoDB engine in case of MySQL).

  23. Thanks for this article.
    I had the same problem as zoltan. Imho validation is very important, including foreign key constraints (to follow your example, on a simple accounts/add form (form without multiple models) you will want a select tag with available companies. Well, it should be validated that when the user submits the account is bound to a company).

    So anyway, my fix for the problem in this context is to temporarily remove the validation rule (after all, no validation of the FK is needed here as Cake will take care of it):
    unset($this->Company->Account->validate[‘company_id’]);
    $this->Company->saveAll($this->data, array(‘validate’=>’first’));

  24. Hi, I am a newbie in cakephp. At first thanks for your nice post. it has helped me a lot and It showed me a way while I was deadlocked.
    I have a sales entry form and I used dynamic table there by which I am taking sales entry several items entry. The user can add/delete any entry without saving into database.
    I needed to edit this form. And after read your article I could manage the edit action.
    while editing the user can add new items which reflects into the database but a former item is removed then it is not being deleted from the DB.
    Is there any way to delete an item which is not in the data array while editing by saveAll()?

Leave a reply to Steven Wright Cancel reply