14

I'm trying to set up an item model and a tag model that have a many-to-many relationship (items have multiple tags and tags belong to multiple items). I'm using Rails and Backbone.js, so I need to have them store, retrieve and update models seamlessly between each other. I would also love it if I could save a new list of tags for a specific item in one go from the client.

What's the correct way to structure the models and controllers on the Rails side and the models on the Backbone side to keep the system RESTful and make it easy to share models between them? Specifically, what would the API look like on the server, and what would the JSON representation of the models be in saving and retrieving them?

I would really appreciate any advice on structure, and I don't really need any code or implementation details -- just a high level setup would be great. Thanks!

Chetan
  • 46,743
  • 31
  • 106
  • 145
  • 2
    Good question and I'm looking forward to reading some answers. As a short note: I've noticed that it rarely makes sense — while maybe counterintuitive — to try to mirror the Rails associations with Backbone. Often the requirements on the client side may appear more complex than they actually are, especially to a dev who knows about the structure of the app. In many cases the Backbone models/collections don't have to be "connected" and a simple finder method to retrieve associated objects will do fine. Just my 5 cents. – polarblau Jul 11 '11 at 11:31
  • So do you suggest that I treat them separately, and make the necessary adjustments to the Backbone models to make it work with Rails? In that case, how do I make it so I can save a new list of tags for a specific item in one go RESTfully with Rails? – Chetan Jul 11 '11 at 18:18
  • To answer my own comment, I can save a list of tags for an item using `accepts_nested_attributes_for` (http://stackoverflow.com/questions/4698266/creating-multiple-resources-in-a-single-restful-post-in-rails). – Chetan Jul 12 '11 at 05:31

2 Answers2

7

Looks like you found your rails answer. Maybe I can help with the backbone side:

Backbone has 2 model constructs: The Model, and the Collection (the collection just being a list of models). There is no formal way of describing relationships with backbone (afaik), so you have to do it yourself. I think what I would do to handle this structure would be 3 collections:

ItemCollection

The item collection would hold all of your items, and each item would, in turn, have it's own TagCollection, which holds the tag models that are related to it.

ItemCollection.TagCollection

Holds references to the main TagCollection instance, but is a local list for this Item only. Since you can '.add' a model to a collection, then you can have multiple collections with the same models populating them.

TagCollection

The TagCollection holds your tags. It's the "main" list of tags that every ItemCollections TagCollection would reference.

For example: You have 3 tags in your TagCollection, and 2 items.

  • item_1.TagCollection has tag_A and tag_B
  • item_2.TagCollection has tag_A and tag_C

If, item_1 then has tag_C added to it, you would simply: item_1.TagCollection.add(tag_C) Similarly, removing: item_1.TagCollection.remove(tag_C) would remove it from item_1 collection, but not any others.

Regardless of the methods you utilize, you'll need to write some code in order to have it do mass updates / creates. Remember that backbone just passes the attribute list along as a JSON string in the body of the request when it does a sync. It doesn't care what it sends. So, so long as your controller was setup to accept a list (1 or more) on it's create method, you should be able to do this pretty simply by doing TagCollection.create([list of tags]). The difficult part would be to override the backbone sync to handle the successful creation, and turning [list of tags] into individual models for the collection.

Hope that helps!

Chetan
  • 46,743
  • 31
  • 106
  • 145
Craig Monson
  • 492
  • 4
  • 9
  • Just wanted to say thanks for this novel and perfect approach. – dclowd9901 Feb 08 '12 at 15:22
  • So if you want to do a client-side search for items with a certain tag, without searching the TagCollection of every Item, it seems reasonable to also have an ItemCollection for every Tag. – Andrew Lundin Oct 02 '13 at 00:44
  • 2
    Also, useful to know: when you add a model to multiple collections, `model.collection` will always be the first collection it was added to, unless you explicitly change it. But when the model is destroyed, it will automatically be removed from all collections. – Andrew Lundin Oct 02 '13 at 01:41
5

[In addition to Pope's answer:]

For reference, the Rails answer (from Creating multiple resources in a single RESTful POST in rails) is to use accepts_nested_attributes_for:

class Item < ActiveRecord::Base
  has_many_and_belongs_to :tags
  accepts_nested_attributes_for :tags
end

class Tag < ActiveRecord::Base
  has_many_and_belongs_to :items
end

The following assumes that you've added ActiveRecord::Base.include_root_in_json = false to one of your initializers (see here for why).

To save a list of tags for an item from Backbone, the answer (from Saving nested objects with Rails, backbone.js, and accepts_nested_attributes_for) is to override sync on the Item model:

sync: (method, model, options) ->
    data = JSON.stringify model.toJSON()
    if (method == "create" || method == "update")
        json = model.attributes
        json = _.extend json, {tags_attributes: model.tags.toJSON()}
        data = JSON.stringify json

    options.data = data
    options.contentType = 'application/json'
    Backbone.sync method, model, options

This solution might require a bit more hackery to get Rails to understand Backbone, but this is how you would start setting them up.

Community
  • 1
  • 1
Chetan
  • 46,743
  • 31
  • 106
  • 145