Notes on CakePHP HABTM (Part 2, saving data)

If you haven’t read part 1, where I cover some HABTM basics, you should probably take a look at that article as well.

Part 2. Saving data

The next topic I wanted to cover is how to go about saving the HABTM data. Actually it is really not that complicated, as most of the work is taken care of for you.
Probably the most important thing is to make sure that your data arrives in the correct format for the save() method. The rest is really pretty easily handled by cake.

So let’s take a look at a few examples (still using our Post HABTM Tag)…

First, we’ve got a case where we know a Post ID and we are creating a single, new tag for it. We’d like to save the tag name and have CakePHP automagically associate it with a given post.

The form would look something like this:

echo $form->create('Tag');
echo $form->input('tag');
echo $form->input('Post.id', array('type'=>'hidden', 'value'=>5));
echo $form->end('Add tag');

And the controller action would be:

function add() {
$this->Tag->save($this->data);
}

Let’s also take a look at our data:

Array
(
    [Tag] => Array
        (
            [tag] => new one
        )

    [Post] => Array
        (
            [id] => 5
        )

)

To clear things up quickly, CakePHP will insert the new tag, grab the ID of the last insert and save the ID of the known post plus the ID of the newly created tag into the join table.

Now, let’s say you need to associate that same tag with a few different posts. Well, the only thing you need to do is to, again, make sure that you have correct fields in your form… so:

echo $form->create('Tag');
echo $form->input('tag');
echo $form->input('Post.1', array('type'=>'hidden', 'value'=>5));
echo $form->input('Post.2', array('type'=>'hidden', 'value'=>6));
echo $form->input('Post.3', array('type'=>'hidden', 'value'=>7));
echo $form->end('Add tag');

Will produce a data array that looks like:

Array
(
    [Tag] => Array
        (
            [tag] => my brand new tag
        )

    [Post] => Array
        (
            [1] => 5
            [2] => 6
            [3] => 7
        )

)

Basically this will save a new tag and associate that newly created tag with posts, which happen to have ID’s 5,6 and 7. I should mention that in the previous example the field could be named Post.1 instead of Post.id, but I find that ‘id’ is more descriptive and works just as well.

Also, in the example I’m using a hidden field with a given value for the ID, in the real life it would probably be a few checkboxes, with values for ID’s.

And lastly it’s worth to note that the above can be reversed, just as well you could setup a quick form to create a new Post and associate some known tags with it.

P.S. Be sure to check out Habtamable behavior, which helps to avoid some of the pain when working with HABTM models.

m4s0n501
  • GrandiJoos

    Hi,

    First of all, great article!
    I do have a question however :)
    If I would like to have a list of posts to add a tag to, to keep with your example. But the list of posts is loaded from the database so we don’t know how many posts there would be, one would most likely use a select list for that with multiple selections, right?
    The problem then is that the resulting data array looks like this (I tried):

    Array
    (
    [Tag] => Array
    (
    [tag] => my brand new tag
    )

    [Post] => Array
    (
    [id] => Array
    (
    [1] => 5
    [2] => 6
    [3] => 7
    )
    )
    )
    with an extra array called id in the post array.
    How would you create the form/action so that it can be easily saved?

    Thanks in advance! Keep the great posts coming!!!

    GrandiJoos

  • teknoid

    @GrandiJoos

    I assume that you are doing find(‘list’) in your controller, and assigning a $posts variable for your $form->input().

    Then getting the data in the right format, is very easy:

    echo $form->input(‘Post’, array(‘type’=>’select’, ‘multiple’=>’checkbox’, ‘options’=>$posts));

    If you don’t like check boxes, you can certainly use a multi-select input instead (‘multiple’=>true)

  • GrandiJoos

    @teknoid

    thnx for the code.
    I already had a multi-select but thanks to your post I found the error.
    I named the input ‘Post.id’ instead of just ‘Post’.

    Now it works!!!

    Thank you!!!

    GrandiJoos

  • teknoid

    @GrandiJoos

    Good to hear. You are welcome.

    I feel like this is a common problem, and could be a good enough reason to request an enhancement ;)

  • den

    Hello,
    I’m a CakePHP newbie, and am really struggling with HABTM. I’m hoping you can set me straight on my issue.

    I have 2 objects, Show and Category. Category can contain many shows, and a show can contain many categories. Thus, I’ve set up each object to have a HABTM relationship.

    Now, I am working in a cronjob mode, where I am building a category list for a show. I have an array of category IDs:

    $categories = array(1,2,3);

    I want to save the list to a specific show, so I am doing (in my controller):

    $data = array(‘Category’ => $categories);

    $this->Show->id=8;
    $this->Show->saveAll($data);

    However, I am having NO LUCK! Nothing ever gets saved into my join table: shows_categories

    Any suggestions? THANKS in advance

  • teknoid

    @den

    saveAll() doesn’t work on HABTM (at least for now). save(), however, does exactly what you need.

    Just format your data correctly:

    $data[‘Show’][‘id’] = 5;

    $data[‘Category’] = array(1,2,3,4);

    1,2,3,4 is your $categories.

    Then you just… $this->Show->save($data);

  • den

    Geeesh…I struggled with this for what seemed like hours. THANK YOU

  • http://www.kienpham.com Kien Pham

    Thanks for the article!

    – Kien

  • teknoid

    @Kien Pham

    You’re welcome.

  • David Wu

    Quick question. What if all I want to do is add a relationship to an existing HABTM item? For example, let’s say I have a Post and now I want to edit that post and add another existing Tag to that post.

    Ex.
    Post id = 1
    currently has HABTM to Tag id = 1

    Now I want to edit Post id = 1’s tags and add
    Tag id = 2. How do I do that?

    Whenever I try the above, Tag id = 1 is overwritten in the join table and is updated to tag id = 2. I want to insert a new tag id not modify the existing one.

  • teknoid

    @David Wu

    You can save data directly into the join table i.e. $this->Post->PostsTag->save($someData);
    Where $someData would have your post_id and tag_id.

  • Jason

    How can i save multiple NEW relations at once? Where Appointment HABTM Users. If the user (email) does not exists, create it, else link existing user to my new appointment.

    Array(
    ‘Appointment’ =>array(
    ‘id’ => 6
    ),
    ‘User =>’Array
    (
    [0] => Array
    (
    [email] => user1@mail.com
    [name] => One
    )

    [1] => Array
    (
    [email] => user2@mail.com
    [name] => Two
    )

    [2] => Array
    (
    [email] => user3@mail.com
    [name] => Three
    )
    )
    )

    Thank you

  • teknoid

    @Jason

    This is not currently possible for HABTM. The best you can do is loop through the User array and do the save() for each record. Possibly saveAll() will allow this in the future.

  • suman

    hi, one question related similar to this.
    I have two tables employees and accreditations and a join table employees_accreditations. Now I want to add new Employee, he selects the accreditation license. when he submits add then new employee should be created in employee table and new records employee_id, accreditation_id,issue_date,expiry_date is created in employees_accreditations.
    how it is possible?

  • suman

    ok in above problem if we don’t know the post_id then??

  • teknoid

    @suman

    You’ll probably get better response if you post your questions at the google group or the IRC channel.
    That being said, you should be able to temporarily bind Employee to your join table model as a hasMany, this way you could use saveAll() although I’m not quite clear on where does issue_date and expire_date is coming from.
    Also, I’m not sure what post_id you’ve referred to…

  • http://www.innovationsopen.com mzee.richo

    Nice stuff @ now , been struggling with this shit .
    got it now

  • teknoid

    @mzee.richo

    Glad it helped ;)

  • Brenton

    Thanks for the tips, got this working, but doesn’t seem to insert ‘created’ date / time field. :(

  • teknoid

    @Brenton

    No problem. It should work, but then again depends on what table you are referring to…

  • Brenton

    @teknoid

    Woops, it’s the associated table that’s not getting the ‘created’ field. So in your example, Post HABTM Tag, ‘created’ in Tag doesn’t get a value.

  • teknoid

    @Brenton

    Strange… does it work if you just try and save some tag directly into the Tag model (table)?

  • Brenton

    @teknoid:

    Nope:

    $this->Post->saveAll(array(‘Post’=>array(‘name’=>’Test’), ‘Tag’=>array(1,2,3)));

    And `posts_tags` has created & modified fields. Oddly enough, if I do `saveAll()` it “works” – the created/modified fields are updated; however, as noted before, it doesn’t save the vital information (ex: post_id & tag_id, they’re null).

  • teknoid

    @Brenton

    I have another post, which shows how to save extra data into the join table.
    I’m afraid that magic fields for the join table do not work, as it is not something that’s expected in the join table.

  • Brenton

    @teknoid,
    That’s what I figured, m’oh well.
    I’ve seen the other post, and actually most of your CakePHP related posts, they’re very informative, thanks for all of them, you’ve been a tremendous help and it’s greatly appreciated. Keep up the good work.

  • teknoid

    @Brenton

    Sounds good and thank you ;)

  • Grover

    Good article! thanks :)

  • http://teknoid.wordpress.com teknoid

    @Grover

    You’re welcome ;)

  • JakZ

    Hey first of all I found out from your article that your really good working with cake.

    I dont have a question particulary about ur post but i was wondering if you could help me out a bit in a problem i have in cake.

    The problem is the following: I need to build up a report of a consignation of books. For this I mean that im gonna ‘lend’ some books to a client and wait for him to return em those which he didnt sell. I got my consignation model in cake, but the final report i need to build must look like something like this:
    id
    client_id
    date
    {

    a list of books AND quantities of each that the client is taking with him

    }

    I got a table named consignations that has consignation_id and date
    I also have a join table that relates consignations_books and is structured:
    id consignation_id book_id quantity

    The problem i have is that i cannot find a way to store the relation for the consignation. I mean, i can create a consignation but i cant find a way to assing a list of books & quantities for each book.

    Working with php and not with cake this table would look like: (consignations_books)
    id consignation_id book_id quantity
    ————————————————–
    1 1 12 7
    2 1 25 2
    3 1 14 2
    4 2 15 3
    5 2 14 8

    This would mean that the consignation #1 (which obviously has a client related) is composed of a loan of 3 different books (listed byt their ids): 12, 25 and 14. And that we loaned 7, 2 and 2 books of each respectively.

    As i mentioned b4, the problem is that I cant happen to do this in any way. I gotta tell you that i havent created any model for consignations_books table…

    Do you have any suggestion of how could i be doing this?

    I will hugely thank you for ur help here ^^

    Sry for being such a rookie here =(

    Thnx!

  • http://teknoid.wordpress.com teknoid

    @JakZ

    If I understand correctly you are trying to save “extra data” in the join table… if so, cake does not allow you to do that (yet) in the same way you’d do it for HABTM. I do have another post somewhere showing how to save extra data by using saveAll().

    If not, please also ask at the google group as I cannot always give you a proper/timely answer.

  • JakZ

    First of all Thank you for replying so quickly. I must tell I wasnt expecting that. Im kinda new into communities and I had heard you were all ppl willing to help but it is until now i beleive it and I gotta tell you thats great.

    I didnt know you couldnt store any extra info in the join tables.. that kinda sucks. Im gonna check the saveall() ur mentioning. But also, is there any other way i could do this? I mean, is there any way i could design my database to work like i want to?

    Thnx

  • Hugo T.

    It’s amazing ! It’s work on the first try !^^
    Thanks a lot !

    By :)

  • http://teknoid.wordpress.com teknoid

    @JakZ

    You can rather easily store any extra data in the join table, just the last time I checked it wasn’t possible directly while storing HABTM. That being said, it’s literally just an extra line of code and slightly different approach, so no need to worry about having to do something overly complex (it’s all cake) ;)
    http://teknoid.wordpress.com/2008/09/24/saving-extra-fields-in-the-join-table-for-habtm-models/

  • http://teknoid.wordpress.com teknoid

    @Hugo T.

    Nice :) I was hoping it would… j/k

  • JakZ

    Well I’m checking the link already.

    Thank you for your reply. Farewell.

    Let’s cake it!

  • JakZ

    Hey guys. Howdy again. Abusing again of your great help I’ve got another question. I’m defining relationships between my model ‘books’ and another model that is called ‘returns’. What I want to end up doing is that when a ‘return’ has a book on it and certain quantity, i want it to automatically return that quantity of that book into my inventory. I wanna do the same for ‘consignations’ but this relation will decrease the inventory.

    IS there any way to specify to cake if it has to increase or decrease the quantity?

    Thnx a bunch again!! C ya guys!

  • http://teknoid.wordpress.com teknoid

    @JakZ

    As much as I would like to help, this really isn’t a place/forum for such questions. You’ll get better response if you post at the google group or the IRC channel.

  • Soni

    Hi…
    i have a little question,
    if i have many tag record, and i want to add all tag record to table posts_tags every time i add new post record,
    how to modify the add post controller

  • http://teknoid.wordpress.com teknoid

    @Soni

    If you know the ID’s of tag, than it’s done as shown in the example.
    If you don’t, you’ll need to save models individually in a transaction as saveAll() does not yet work with HABTM models. Unfortunately there is no very easy answer, but if you ask at the google group or search around this blog, you’ll find some examples of how it’s done.

  • http://www.nbruns.us ItsFridayMoanin

    First of all, thanks a lot! This post was very helpful in figuring out this tricky method (for cakePHP n00bs like me).

    There are several cases to consider in storing HABTM relations (many new tags to one existing post, one new tag to many existing posts, existing tags from a checklist to a new post, etc.) and they don’t quite all get covered in the $this->Post->save($this->data) the way we’d like them to! That’s OK, though, I empathize with the great programmers at cakePHP on this really complex scenario. It wouldn’t be magic if it was easy!

    Anyway, my situation is this: I wanted to have one form that allowed adding multiple pre-existing tags to an existing post AND allow the user to add a string of comma separated NEW tags (like Jason wanted). I made the form like you suggested to GrandiJoos (with a row of checkboxes) and then just added a text field called “tagCSV” for the csv string of new tags. Adding this text field gave me a problem similar to Grandijoos where my Tag array with my checked tag_id’s and my csv string was buried inside another array called Tag. (“Wha…dangit!”) pr($this->data) is your friend! Use it!

    Then I used your suggestion to loop through the multiple new tags and add them individually. Here is the fleshed out code of that suggestion:

    if(!empty($this->data)) { // PROCESS DATA FOR ENTRY
    if(!empty($this->data[‘Tag’][‘tagCSV’])) {
    foreach(explode(“,”, $this->data[‘Tag’][‘tagCSV’]) AS $i=>$k) {
    $data = $this->data;
    // $data[‘Tag’][‘Tag’] is the list of checkboxes – this step might not be necessary
    unset($data[‘Tag’][‘Tag’]);
    $data[‘Tag’][‘tag’] = trim($k);
    $this->Tag->saveAll($data);
    array_push($this->data[‘Tag’][‘Tag’], $this->Tag->id);
    }
    }
    unset($this->data[‘Tag’][‘tagCSV’]); // also might be unnecessary
    // adding the csv text field makes this step necessary
    $this->data[‘Tag’] = $this->data[‘Tag’][‘Tag’];
    $this->Tag->Post->save($this->data);
    }

    Hope this helps somebody, this took me a couple days to figure out.

    Oh, also something that wasn’t completely obvious to a n00b like myself – all this goes on an function_name.ctp page in your /views/tags/ folder. And if you use a function_name different than add you need to change your $form attributes.

    OK, that’s it! Gotta leave some stuff to figure out on your own!

    Thanks again!

  • http://www.arielsanguinetti.com.ar Ari Sanguinetti

    Hi!

    i can’t save a HABTM relation between the same object, for example when a post is child of other post, please HELP!

  • http://teknoid.wordpress.com teknoid

    @Ari Sanguinetti

    It’s pretty hard to guess, based on what you’ve shared…

  • Chirayu

    I am having HABTM relation with “user” and “groups” in my social network site as user can join multiple groups and multiple users are in one group. Now a user is creating at group needs to be default group member. so in relation table i need to insert group_id and user_id after creating a group by user. I can have user_id but how can I get recently created group_id. or any clue for that? my IDs are UUID for group and users.

  • http://teknoid.wordpress.com teknoid

    @Chirayu

    Try the habtamabale behavior, which will let you save both models at once…
    Or, you could save the group first. Then your group ID, will be available in $this->Group->id;
    Once you have the ID simply save both user and group id’s to the join table.

  • http://www.w3answers.com aniltc

    I have table organization,address and addresses_organizations table.My question is how can I save data to database using HABTM relation.

    my relation as it is

    class Organization extends AppModel {
    var $name = ‘Organization';
    var $hasAndBelongsToMany = ‘Address';

    }

    class Address extends AppModel {
    var $name = ‘Address';

    var $belongsTo =array(‘Zipcode’);
    var $hasAndBelongsToMany = array(
    ‘Organization’ =>
    array(
    ‘className’ => ‘Organization’,
    ‘joinTable’ => ‘addresses_organizations’,
    ‘foreignKey’ => ‘address_id’,
    ‘associationForeignKey’ => ‘organization_id’
    )
    );

    }

    I have created the form where I have organization details as well as address details.I can only insert into organization table but not into other two tables. I think I am wrong in the creation of form feilds.I am not passing any ids to the controller during the save operation.I have one more question can I use savell() here.

  • http://teknoid.wordpress.com teknoid

    @aniltc

    I have some other posts, which show how to save two HABTM models at once.

  • fralik

    Nice article! Do you know how to delete associated data properly?
    I see only two possibilities:
    1) remove all data and create new associations;

    2) find out what should stay and what should go and update associations accordingly.

  • http://teknoid.wordpress.com teknoid

    @fralik

    I’m not sure if this is what you are after exactly, but there is a ‘unique’ key, which you can define in the association, which will handle association deletions automagically. Well, I guess you could experiment with that a little.

    p.s. I firmly believe in soft-deletes only (i.e. set active 1 or 0 in your DB). If hard deletes are truly required log-in to your DB and do the delete as needed… I guess it doesn’t apply to all cases, but just something to consider.

  • http://www.lifeyoumentary.com Phaz

    Just wanted to say thanks! spent 2 days trying to figure out why my HABTM wouldn’t save. A simple search turned up your properly constructed array and voila! success!

    Thanks again!

  • http://teknoid.wordpress.com teknoid

    @Phaz

    Good to hear, thanks.

  • Pingback: Tag Time: CakePHP Tag Plugin()

  • triklosan

    Great! Thanks.

  • willians

    please people how save this, relations of my models is HABTM

    Array
    (
    [News] => Array
    (
    [titulo_contennoticias] =>
    [sumario_contennoticias] =>
    [texto_contennoticias] =>
    [status_contennoticias] => 0
    [categorias_id_categorias] => 1
    [usuario_id_usuario] => 1
    [videocodigo] => Comercial_ejemplo1.flv
    [galeriacodigo] =>
    )

    [Video] => Array
    (
    [0] => Array
    (
    [videos_id_videos] => 2
    )

    [1] => Array
    (
    [videos_id_videos] => 1
    )

    )

    [Gallery] => Array
    (
    [galerias_id_galeria] =>
    )

    )

    I trying with saveAll but in this section:

    [Video] => Array
    (
    [0] => Array
    (
    [videos_id_videos] => 2
    )

    [1] => Array
    (
    [videos_id_videos] => 1
    )

    )

    only save the videos_id_videos 1, dont save videos_id_videos 2 and videos_id_videos 1

    please help!!!!!

  • teknoid

    @willians

    You are trying to save two habtm models at once. Or you straying way off conventions. It is not supposed to work with the way your data array is formatted.

    I have other posts showing how to save two habtm models and some tricks you can use to save one and another in the join table.

  • Jorge

    Hi,
    I have a problem saving the data in a habtm relationship. In my table, I have a key like integer and other like char. when I save the data, my table messages_recipients is empty.
    Anyone have an example of HABTM using char like IDs??
    My array:
    Array (
    [Message] => Array (
    [username] => jorge
    [title] => just a title
    [body] => asdf
    )
    [Recipient] => Array (
    [Recipient] => Array (
    [0] => admin
    [1] => jorge
    [2] => test
    )
    )
    )

  • teknoid

    @Jorge

    Your array looks good and mixed keys should not be an issue.

    The other things to check is failing validation, any beforeSave() not returning true (and finally, if able, check SQL debug).

    • Jorge

      thank you for you help, but checking the sql logs, the insert for my join table is missing.
      Any example with hatbm with char like PKs???

      INSERT INTO `kdo_messages` (`username`, `title`, `body`, `modified`, `created`) VALUES (‘jorge’, ‘fyjhr r’, ‘tyer’, ‘2011-06-09 19:03:23′, ‘2011-06-09 19:03:23′)

      SELECT LAST_INSERT_ID() AS insertID

      SELECT `MessagesRecipient`.`username` FROM `kdo_messages_recipients` AS `MessagesRecipient` WHERE `MessagesRecipient`.`message_id` = 3

      SELECT `User`.`username` FROM `kdo_users` AS `User` WHERE 1 = 1

  • teknoid

    @Jorge

    Right, but did you check the above issues (validation, beforeSave(), etc.)?
    Also… if you are using MyISAM table type for MySQL it will not support transactions, so maybe one more thing to look out for.

    Other than that it’s hard to say….