zen of coding

Practical use of saveAll() (part 2, notes and tips)

In part 1, I explained how to use saveAll() in order to save multiple models at the same time, if you haven’t had a chance to read that post please do so before starting on this one.

1. Saving multiple records for the same model.

Using our models from the previous example, we can build a form to save multiple Accounts at the same time (accounts/add.ctp):

echo $form->create();

echo $form->input('Account.0.name');
echo $form->input('Account.0.username');
echo $form->input('Account.0.email');
echo $form->input('Account.0.company_id', array('type'=>'hidden', 'value'=>1));

echo $form->input('Account.1.name');
echo $form->input('Account.1.username');
echo $form->input('Account.1.email');
echo $form->input('Account.1.company_id', array('type'=>'hidden', 'value'=>1));

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

Now, in your Accounts controller’s add() action you would do something like this:

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

Notice, that I am passing $this->data[‘Account’] to saveAll(), rather than just $this->data. This is necessary for saveAll() to work properly when saving multiple records for a single model.

2. Any chance to save deep bindings?

No, at least currently, if you have Company->Account->Profile, you cannot save all three models in one go.

3. What if Company hasMany Account and Company hasMany Profile?

This is possible, you can save all three models at the same time. It is very similar to the way we’ve saved Company and Account models before. Just add Profile model’s fields to the form, named as Profile.0.name (for example).

4. Make sure your DB supports transactions

You’ve probably noticed that saveAll() uses transactions to ensure data integrity. I, however, made a little mistake (https://trac.cakephp.org/ticket/5178) and forgot that MySQL’s default MyISAM storage engine does not support transactions. So, make sure that you use InnoDB in MySQL to enable transaction support (if you use another DB, double check that transactions are supported).

5. Is it possible to save HABTM models with saveAll()?
Update (3/30/2009): It is now working just fine, please see this post for more details.

I have not found a way (or a supporting test case) to save multiple HABTM models at once, for example, if you wanted to create a new Post and some new Tags and save all that goodness in one shot.
It is possible with save() to create a new post and assign some Tags to it, but you need to know the ids of the Tags before the save(). (See this post on saving HABTM data for more details).

6. What if Company hasOne Account?

This is still possible, but since you can only have one Account per Company, you should name your Account model fields as usual, i.e. Account.name (no need for Account.0.name, since only one Account is allowed by the relationship).

Well, that pretty much concludes the majority of things you should know about saveAll(), hopefully now, you should be able to handle and troubleshoot a variety of cases where saving of multiple models or records is needed.

  • Shannon

    Thanks for your blog. It is greatly appreciated. I have a question… With my experiments with saveAll, I have discovered that it saves the blank records along with the records that have data entered. For example, lets say I filled out the first account record with data but left the second one blank. The saveAll will save both records, inserting the second blank record into the database. Is there an easy way to stop this from happening?

  • teknoid

    @Shannon

    Thank you. I’m glad the blog is helpful.

    The saveAll() situation you’ve described should not be happening. It’s possible that it has to do with transaction support in your DB, but it’s hard to tell without an example.

    I suggest you stop by the IRC irc://irc.freenode.net/cakephp channel, where it would be easier to troubleshoot that problem.

  • Thanks for the tip about saveAll using database transactions and needing to use InnoDB.

  • teknoid

    @Marc Grabanski

    You’re welcome.

  • Lane Olson

    Great article, this will definitely cut down the amount of code I have to write. However, I have encountered a problem. For some reason if I enter a value in one of the fields that violates a validation rule, the form does not validate (as expected), but I do not get the validation error back. When I print out $this->Account->validationErrors I get and array like this:

    array(
    [0] => (
    ),
    [1] => (
    )
    )

    Any idea why the fields and error messages are not showing up in the array when I use saveAll? Instead I had to do something like this in order for it to display the error messages:

    foreach($this->data[‘Account’] as $k => $v)
    {
    $this->Account->set($v);
    if(!$this->Account->validates())
    {

    $errors[$k] = $this->Account->validationErrors;
    }
    }

    if(empty($errors))
    {
    // All records have validated, it is safe to save
    $this->Account->saveAll($this->data[‘Account’]);
    }
    else
    {
    $this->Account->validationErrors = $errors;
    }

  • teknoid

    @Lane Olson

    Thanks.

    Not sure, you should ask at the google group mailing list, probably a better place to get help with something like that.

  • Lane Olson

    @teknoid

    Alright, will do. Just curious… did you have this problem at all?

  • teknoid

    @Lane Olson

    Nope, I’ve never seen it.

  • TK

    @teknoid Thanks for the useful articles regarding saveAll function.

    I just wanted to mention that I was getting the exact same problem as Lane on PHP 4 Environment (V 4.3.9) yet when I test the same script on PHP 5 Environment (PHP 5.2.6) it works flawlessly.

    I guess I really should upgrade to the latest PHP 4 stable or move onto PHP 5…

  • teknoid

    @TK

    Hmm… interesting. Thank you for bringing this up.

  • Alex

    I have a a similar situation, say Company and Accounts (as in part 1). when saving, $this->data looks like:
    [Company] => Array ( [id] => 1 [name] => aaa [description] => bbb )
    [Account] => Array (
    [0] => Array ( [id] => 1 [name] => ccc [username] => ddd
    [100] => Array ( [id] => [name] => eee [username] => fff
    [101] => Array ( [id] => [name] => ggg [username] => hhh )

    Notice the id for Account[0] exists already, so the saveall() will properly do an update, but the id’s for Account[100] and Account[101] don’t exist, so it’ll properly do inserts. How do I get both the Account id’s of the inserted records? note: $this->Company->Account->id only provides the last inserted record

  • @Alex

    Because all records are processed in a transaction, I don’t see any way to extract each ID at a time.
    Although, please double check at the google group, maybe someone can point to a good solution.

  • Alex

    FYI, I ended up not using saveAll() and individually save() each model. Tedious, but it works.

  • @Alex

    Tedious is one thing, no data integrity is another… but of course it depends on your needs.

  • hassan

    one way of avoiding null records could be to delete that particular aray keys before calling the saveall(),
    since saveall() requires those fields in an array in particular format so this trick may work.

  • @hassan

    Could you clarify what “null records” are you referring to?

  • hassan

    as mentioned by “Shannon”, saveall() inserts blank records, null records reffers to those blank records.

  • @hassan

    Are you using saveAll() with validation? Does your DB support transactions (InnoDB tables in MySQL)?

    There are quite a few test cases covering saveAll() and I’ve not come across this case. If you have a more concrete example, perhaps you could share and we could build a test cases to submit as a potential bug, if one does exist.

  • hassan

    i am using the table “posts” that hasMany “images”.

    the view file “add.ctp” is as follow.

    echo $form->create(‘Post’, array(‘action’ => ‘add’));
    echo $form->input(‘Post.title’, array(‘type’=>’text’,’id’=>’textinput’, ‘maxLength’=>’25’));
    echo $form->input(‘Image.0.name’, array(‘id’=>’simage’));
    echo $form->input(‘Post.body’, array(‘type’=>’textarea’,’id’=>’textareainput’, ‘maxLength’=>’25’));
    echo $form->end(“Submit”);

    in controller class, the add function is defined as,

    function add()
    {

    if(!empty($this->data))
    {
    $this->Post->saveAll($this->data, array(‘validate’=>’first’));
    }
    }

    the table for images have fields “id”,”name”,”created”,”modified”, i am just taking tha file name as simple text input for simplification no file upload is done,

    it worls fine when i fill in all fields, ie, title, image name, and body of post, but when i type only two fields ie, the post title and the post body, it also creates a row in images table with and id,created and modified fields keeping the name field empty.

    thats the whole scenario that i faced.

    thanks

  • @hassan

    I take it that the problem is that validation for images should fail, yet it creates a blank record?

    Did you ensure that your DB supports transactions (i.e. in MySQL you have InnoDB engine for both tables). If not, the validation on the Image model may fail, but since the transaction is not properly handled on the DB layer the data will be saved regardless.

  • Man, I love you :)

  • @Ahmed Sabbour

    Well, I guess it’ll be a notch to some heterogenic comment, so… thanks ;)

  • Pingback: Transactions in CakePHP « Myles Kadusale’s Blog()

  • Nachopitt

    Thanks for all this info, great resources. I just wanted to know if you ever had an enviroment when you needed to save multiple rows of a single model, but each row is associated with another model via a hasOne association.

    So you have something like this:

    Company hasOne Account, and you want to save multiple companies, and each company with its own account

    array(
    ‘Company’ => array(
    0 => array(
    ‘name’ => ‘x’,
    ‘Account’ => array(‘name’ => ‘xx’)),
    1 => array(
    ‘name’ => ‘y’,
    ‘Account’ => array(‘name’ => ‘yy’)),
    2 => array(
    ‘name’ => ‘z’,
    ‘Account’ => array(‘name’ => ‘zz’)),
    3 => array(
    ‘name’ => ‘w’,
    ‘Account’ => array(‘name’ => ‘ww’))
    )
    )

    Have you ever meet this setup and used saveAll? I have tried, but with no success. Thanks in advance and sorry about my english.

  • Jesh

    Thank for this article it save my days digging into cake to find out why saving broken in saveAll() due to the limitation of cake on saving deep binding in one step. Sad.

    Instead of saving via loop, is there any behaviors can achieve what i dream?

  • How does it work with $belongsTo? Say the Company belongs to a CompanyGroup, can I update something in CompanyGroup while saving Company?

    (In my particular case, my CompanyGroup $hasOne Company, which is why I want to do it. The question remains for both$belongsTo, whether the reciprocal relationship is )

  • @Anonymous

    It’s the same as what #6 talks about…

  • Ali

    Thanks for the great article, I’m stuck with a problem though.

    I am developing a quiz application where a question can have minimum of 4 answers. So in my views/questions/add.ctp:

    echo $form->input( ‘question’, array(‘label’=>’Question:’, ”type’=>’textarea’) );
    echo $form->input( ‘Answer.1.answer’, array(‘label’=>’Answer 1:’, ”type’=>’text’) );
    echo $form->input( ‘Answer.2.answer’, array(‘label’=>’Answer 2:’, ”type’=>’text’) );
    echo $form->input( ‘Answer.3.answer’, array(‘label’=>’Answer 3:’, ”type’=>’text’) );
    echo $form->input( ‘Answer.4.answer’, array(‘label’=>’Answer 4:’, ”type’=>’text’) );

    So here is the controllers/questions_controller.php

    if ($this->Question->save($this->data[‘Question’])) {
    $this->data[‘Answer’][1][‘question_id’] = $this->Question->id;
    $this->data[‘Answer’][2][‘question_id’] = $this->Question->id;
    $this->data[‘Answer’][3][‘question_id’] = $this->Question->id;
    $this->data[‘Answer’][4][‘question_id’] = $this->Question->id;

    $this->Answer->saveAll($this->data[‘Answer’]);

    $this->Session->setFlash(__(‘The Question has been saved’, true));
    $this->redirect(array(‘action’ => ‘index’));
    }

    So the question can be save, but problem is how do we assigned this new inserted id from questions into all the question id for answer? Right now I have to assign it manually to the 4 answers, I am sure there is a better way. Since I am planning to allow users to add/remove answers, so answers can be less or more than 4, doing it manually doesnt seems right.

    Thanks in advance.

  • @Ali

    saveAll will handle it for you. You just need to pass it $this->data.
    Check the link to part 1 of this post at the top, it explains how to use saveAll() with multiple models.

  • Ali

    Wow … it works like a charm, thanks again! :)

  • Jon

    Thanks for the great article. It helped me get pretty far. I’ve got a question about #2. I have Project hasMany Task and Task hasMany Keyword. In the projects controller, I’d like to save in Project, Task, and Keyword in one go. Any idea if Cake has/will implement this now? If not, what’s the best way to do it myself? Thanks!

  • @ Jon

    There will be no changes to ORM at this point in 1.3, so to answer your question it won’t be possible in one shot.

    The best approach would be to utilize afterSave() of one of the models.

  • I wonder if you have an example of multiple row with hasMany save at once.

    I can save multiple records at once using $this->Company->saveAll($this->data). That already includes its Account with Company->hasMany->Account relationship.

    I got stuck when I need to save multiple company with multiple account at once. Btw, I’m doing the array structure in controller.

    Btw, here’s my array structure http://pastebin.com/pAP5kctB

  • teknoid

    @ianemv

    Try here: http://nuts-and-bolts-of-cakephp.com/2009/03/26/saveall-with-multiple-records-and-for-multiple-models/

    (there were a few updates, which you’ll see in the post. please take a look the links)

%d bloggers like this: