zen of coding

Revisiting saveAll() and HABTM

During my months of winter hibernation, or maybe due to some oversight in prior review of the test cases… I’ve missed a very important and a great feature, which is that saveAll() can also work with HABTM data.

Let’s see a quick example based on the provided test cases.

We have the following models:

Tag HABTM Article
Article HABTM Tag
Article hasMany Comment
… and both Article and Comment belongsTo User

First, we’ll build a form (view add.ctp), so we can store an Article with some Tags as well as relevant Comment, all belonging to some User. All in one go with the lovely saveAll()

<?php
    echo $form->create();

    echo $form->input('Article.title');
    echo $form->input('Article.body');
    echo $form->hidden('Article.user_id', array('value' => $session->read('User.id')));

    echo $form->input('Tag', array('options'=>array(15,20,30), 'multiple'=>'checkbox'));

    echo $form->input('Comment.comment');
    echo $form->hidden('Comment.user_id', array('value' => $session->read('User.id')));

    echo $form->end('Add Aricle with Tags and Comment');

?>

Pretty simple form. I am reading User.id from some variable that was previously stored in the session (you could actually modify the data array in the controller before saving, rather than passing it as a hidden field). Also, I just created a “fake” array of Tag id’s, in ‘options’=>array(15,20,30)… in real life the values would likely come from $this->Article->Tag->find(‘list’);

Once the form is done, the controller action to save all of this data in one shot, couldn’t be simpler:

function add() {
    if(!empty($this->data)) {
       $this->Article->saveAll($this->data);
    }
}

Just for fun here’s the resulting SQL:

[sourcecode language=”sql”]
START TRANSACTION
INSERT INTO `articles` (`body`, `user_id`, `updated`, `created`) VALUES (‘This is a great article with tags and comments’, 5, ‘2009-03-30 11:54:54’, ‘2009-03-30 11:54:54′)
SELECT LAST_INSERT_ID() AS insertID
SELECT `ArticlesTag`.`tag_id` FROM `articles_tags` AS `ArticlesTag` WHERE `ArticlesTag`.`article_id` = 6
INSERT INTO `articles_tags` (`article_id`,`tag_id`) VALUES (6,’0′), (6,’1′), (6,’2’)
INSERT INTO `comments` (`comment`, `user_id`) VALUES (‘NO comment’, 6)
SELECT LAST_INSERT_ID() AS insertID
COMMIT
[/cc]

Pretty powerful stuff, with just a couple of lines of code.

P.S. My previous post, which mentioned the problems with saveAll() and HABTM had been corrected.

  • Webguy

    What about creating a new tag that would be paired with the new article? Is that possible with saveAll?

    • Tyrone

      I have the exact same question. I can’t get it to create(or update) tags(in my case, email addresses) whilst saving habtm related data.

  • @Webguy

    I haven’t seen a supporting test case for something like this and haven’t been able to find a simple (one-liner) way to do that. So I’d have to say no. That being said, there are a few simple workarounds to make that happen.

  • Webguy

    Perhaps you could suggest a simple workaround? I mean, I could use an embedded form (or a popup, etc.) that updates a parent form’s div with the appropriate new key/value pair, but that seems excessive since I believe this is a fairly common case.

    Scenario: you are writing a blog post and you want to tag it with a *new* tag instead of having to do that in a separate process (CRUD operation)… Discuss!

  • I’ve posted a question about that on Cake Google Group some time ago – http://groups.google.com/group/cake-php/browse_thread/thread/0c4fe7de83cf3fc5?hl=pl. I was wondering is it really safe.

  • @Raph

    Why don’t you think it’s safe? Maybe I misunderstood the question, but if your concern is data integrity, please be sure that your DB supports transactions. Then in case one model fails to save, the transaction will be rolled back.

  • @Webguy

    The simplest solution is to save tags first, grab the ID’s and do a saveAll() directly on the JOIN table/model.
    Granted it’s not done in one line (operation) but 3 or so ain’t too bad either.

  • Pingback: Revisitng saveAll() and HABTM | Dailytuts.net - Daily tutorial for peoples()

  • Ponch

    This seems to be exactly what I’m looking for but I can’t get it to work. Would you be kind enough to post how the resulting array would look like for a HABTM save like this one??

    Thanks in advance!

  • @Ponch

    Something like this:

    Array
    (
        [Article] => Array
            (
                [title] => new article
                [body] => This is a brand new article by me.
                [user_id] => 1
            )
    
        [Tag] => Array
            (
                [Tag] => Array
                    (
                        [0] => 1
                    )
    
            )
    
        [Comment] => Array
            (
                [comment] => Please review and comment
                [user_id] => 1
            )
    
    )
    
  • For anyone not getting this to work, there is a very small piece of information on one of the posts linking to this post which makes it work… Change your database table from MyISAM to InnoDB. Once I did that the saveAll() worked perfectly, and easily.

  • bashMaistora

    this is crazy
    echo $form->hidden(‘Comment.user_id’, array(‘value’ => $session->read(‘User.id’)));

  • @bashMaistora

    I think it’s pretty sane…

  • teknoid, I’m a little confused on this post’s wording and the associated comments. In the linked story that this is updating, you crossed out the following:

    “[…] 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().”

    …that makes me believe that it now is possible (create new tags, and save it all in one shot) with HABTM and saveAll(). However, in the comments of this post you say it’s not possible and that you need to know the ids of the tags prior to saving the new post. Isn’t that technically what was being done in the previous article’s example? I don’t suppose you could clarify?

  • Nevermind, I tested this out myself. HABTM will not allow you to save without first having the related items saved. One could however create a behavior to save the records first and modify the data array to make it work (as I have done this manually in my controller during testing).

    I will point out one thing though: As of v1.2.7, when saving data from a hasMany relationship (e.g., Comments of a Post), you must number the elements in the view so that the data array is also keyed numerically:
    echo $form->input(‘Comment.0.comment’);
    echo $form->input(‘Comment.0.author’);

    If you are saving a hasOne or belongsTo associated record, there is no need for the numerical index (and would cause an error).

  • Here’s a link to my quick/futile attempt. :) Again, this is written/tested with CakePHP v1.2.7 so I cannot guarantee compatibility with any other versions.

    http://bin.cakephp.org/view/465841728

  • I feel rather silly. I just saw your post from over a year ago explaining just this same method. http://nuts-and-bolts-of-cakephp.com/2009/04/21/more-pondering-about-habtm-lets-save-new-tags-with-a-post/ Whoops. Sorry!

  • For me, nothing with this kind of numbering works: Model.0.field

    I’m working with HABTM. My array is quite different from your, since my numbering corresponds to the present model, and not to a related model (ei: Tag).

    echo $form->input(‘User.id’, array(‘type’=>’hidden’, ‘value’ => $user_data[‘User’][‘id’]));
    echo $form->input(‘Word.0.name’);
    echo $form->input(‘Word.1.name’);
    echo $form->input(‘Word.2.name’);

    Array (

    [User] => Array (

    [0] => Array ( [id] => 1 ) )

    [Word] => Array (

    [0] => Array (

    [name] => one )

    [1] => Array (

    [name] => two )

    [2] => Array (

    [name] => three )

    ) )

    I also tried not numbering the User entry. And that way, a row is saved in the users_words table, but nothing is being saved in the Word table :o(

    Any clue? I’m using CAKE 1.3.4, with InnoDB. I think i tried it all.

  • teknoid

    @Guillermo

    Try with just “User” i.e. not ‘User.id’

    In the example this is how the array of tags is built.

    p.s. That being said ‘User.id’ should work, but it’s been a while since I’ve last tested that approach (might have changed in the core by now).

  • Thanks, but unluckily that didn’t work.

    Nothing with this format will be saved: ‘Word.2.name’, and only User.id is saved in the relational table.

    I know you wrote the article many time ago. However, it’s the only approach i can find about saving multiple records using HABTM.

    I changed strategy: instead of saving from Words controller, i moved my functions and tried saving from Users controller. But it neither works. Even, i tried unbinding HABTM and binding hasMany… with no luck. I’m quite lost.

  • teknoid

    @Guillermo

    Not sure what else to suggest. Perhaps validation is failing somewhere … or you have a beforeSave(), which is not returning TRUE.

    Other than that I don’t have any ideas…

    • What I find so itenrtesing is you could never find this anywhere else.

  • Ivan

    Thanks man, your post really saved my life… tried 2 days without success to implement transactions in cake… and your explanation is way better than the php cookbook, thanks again your are cake’s god…

%d bloggers like this: