Using AR::Associations to limit find() scope

Tweet

In an ecommerce application I’m currently building there’s a URL for destroying a line item in the user’s cart:
http://127.0.0.1:3000/cart/line-item/16/destroy

Like all good web coding monkeys should, I’m going to need to check that the LineItem with id 16 is actually owned by the user before destroying it. Sounds obvious? Well scarily there are plenty of web applications I’ve seen that don’t do this check, often just assuming database ids passed in via GET or POST are valid.

An initial way to code up the destroy action of the LineItemsController could be:


def destroy
  fetch_order_from_session # sets up the @order object
  @line_item = LineItem.find(params[:id])
  if @order.line_items.include?(@line_item)
    @line_item.destroy
  end
end

another way could be:


def destroy
  fetch_order_from_session
  @line_item = LineItem.find(:first, :conditions => ['id = ? AND order_id = ?', params[:id], @order.id])
  @line_item.destroy
end

These both work, but aren’t exactly pretty. One of the premises of ActiveRecord associations is that if you’ve defined the relationship between two model objects you shouldn’t need to repeat yourself by coding foreign key searches by hand.

A lesser known (and poorly documented) trick is to use the association’s find method:


def destroy
  fetch_order_from_session
  @line_item = @order.line_items.find(params[:id])
  @line_item.destroy
end

The association’s find method works exactly as a regular find, but the SQL select is automatically scoped to the foreign key of the parent. In this case "order_id = #{@order.id}" is added to the SQL SELECT clause. You don’t have to stop with simple finds either, you can perform all your regular find trickery:


@order.line_items.find(:all, :conditions => ["quantity >= ?", 0])

This technique is also useful if you have a user object that’s created by a before_filter. Rather than having to add user_id = @current_user.id to every find, just perform the find directly through an association of the @current_user object. If you’re new to using associations I suggest checking out a previous blog post of mine: Abusing err.. Using AR::Associations.

As a side note, don’t forget to ensure it’s a POST request using ActionController’s verify macro. You wouldn’t want those frenzied GET pigeons messing up your lovely database would you?

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • wwb_99

    Very good point. Any user input should always be sanity checked.

    I would, however, note that one should not be taking actions other than retrieving information using Http GET requests. That should be completely handled using POST. The main reason for this is things like bots can have an interesting effect on things. There was a situation once where google started deleting people’s profile because the delete command was something like “userprofile.php?id=NN&action=delete”

  • http://www.toolmantim.com timlucas

    See my note in the last paragraph ;)

    An example of using the verify macro:

    
    class LineItemsController < ActionController::Base
      verify :method => :post, :only => :destroy
      def destroy
        ..
      end
    end
    
  • http://www.toolmantim.com timlucas

    I forgot to mention there’s also the ScopedAccess and MeantimeFilter plugins which help limiting the scope of database fetches. Only problem with using plugins though is you’re not guaranteed they won’t break on future versions of Rails, and the technique I described is useful elsewhere.

  • Stevenwulf

    What if the order’s lineitem hasn’t been saved and is only in memory. will the find still work?

  • http://www.toolmantim.com timlucas

    Stevenwulf: Nope, the find is only for database fetches. If you were storing it in memory (in the session for example) then you’d probably be referencing it by position in the line_items array, rather than the record’s id.

    For this app I’m creating the order in the database with a state of ‘unfinished’. I store the session id with the order, so when I delete old sessions from the database I also delete their unfinished orders.