Using AR::Associations to limit find() scope

    Tim Lucas
    Share

    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?