Backbone.js and Rails has_many :through Relationships

The has_many :through relationship is a core relationship for most rails apps. Implementing a Backbone front-end into your app requires mirroring your many-to-many back-end model relationship in Backbone. While Backbone-relational provides a great framework for handling model relationships in Backbone, I wanted to see if I could get it working on my own with only the bare, necessary functionality.

First, the Rails models. I will use a typical shopping cart example:

class Order < ActiveRecord::Base
  has_many :line_items
  has_many :products, :through => :line_items
  accepts_nested_attributes_for :line_items, :allow_destroy => true
end
 
class Prodcts < ActiveRecord::Base
end
 
class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
end

So an Order has many Products through the LineItem join model. In this many-to-many scenario I only care about the Products an Order has, and do not need to know the inverse–what Orders a specific Product has been in. For this reason, I am not setting up any relationships in the Product model.

Now to set up the Backbone model structure. In Backbone, the model relationships will not be a 1-to-1 mapping of the rails model relationships. In Backbone we simply need to worry about an Order containing a collection of Products. No need for a join model. See this post for a more detailed explanation.

Backbone models (in CoffeeScript):

class Product extends Backbone.Model
  paramRoot: 'region'
 
class ProductsCollection extends Backbone.Collection
  model: Product
  url: '/products'
 
class Order extends Backbone.Model
  initialize: ->
    @products = new ProductsCollection()
 
  line_items_attributes: ->
    @products.map (p) ->
      {product_id: p.get("id")}
 
  toJSON: ->
    json = {order : _.clone(@attributes)}
    _.extend(json.order, {line_items_attributes: @line_items_attributes()})

Two important points here. Our Order instances initialize to have an attribute @products, which is just a Backbone Collection to hold all of its related products. This will emulate a one-to-many relationship.

Second, we are overwriting toJSON() in order to structure the JSON data so that it’s useful to rails. More on that later.

Next let’s put the Backbone models to use. I can add Products to an Order using the add() funciton.

@order = new Order()
@order.products.add([@product1, @product2])
@order.save()

When the order is saved, the overwritten toJSON() function will also include the order’s product data. The JSON data needs to be structured in such a way as to tell rails to create a new Order record, and new LineItem records for each product in the order.

{"order"=>
  {"user_id"=>99, "line_items_attributes"=>
    [{"product_id"=>6}, {"product_id"=>11}, {"product_id"=>26}]
  }
}

Setting

accepts_nested_attributes_for :line_items

in the Order moddel allows it to create both Order records and associated LineItem records in one JSON post.

Since rails only needs to know the ids of each Product we wish to associate to an Order, the products.add() call can be simplified to only contain id data.

@order = new Order()
@order.products.add([{product_id: @product1.id}, {product_id: @product2.id}])
@order.save()

This will ensure our JSON data is as clean and simple as it needs to be in order to create the association records.

This is a very rough first pass at getting Backbone to play well with a Rails has_many :through set-up in one direction. I haven’t covered how to load existing Orders into Backbone for view rendering. This will require customizing the as_json method in rails to include it’s related Product data. I hope to cover that in a part 2 post. Please fill me in on other approaches you have come up with to handle Backbone model associations.