• Home

  • Custom Ecommerce
  • Application Development
  • Database Consulting
  • Cloud Hosting
  • Systems Integration
  • Legacy Business Systems
  • Security & Compliance
  • GIS

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Blog
  • Careers

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    Association Extensions in Rails for Piggybak

    Steph Skardal

    By Steph Skardal
    October 31, 2012

    I recently had a problem with Rails named scopes while working on minor refactoring in Piggybak, an open source Ruby on Rails ecommerce platform that End Point created and maintains. The problem was that I found that named scopes were not returning uncommitted or new records. Named scopes allow you to specify ActiveRecord query conditions and can be combined with joins and includes to query associated data. For example, based on recent line item rearchitecture, I wanted order.line_items.sellables, order.line_items.taxes, order.line_items.shipments to return all line items where line_item_type was sellable, tax, or shipment, respectively. With named scopes, this might look like:

    class Piggybak::LineItem < ActiveRecord::Base
        scope :sellables, where(:line_item_type => "sellable")
        scope :taxes, where(:line_item_type => "tax")
        scope :shipments, where(:line_item_type => "payment")
        scope :payments, where(:line_item_type => "payment")
      end
    

    However, while processing an order, any uncommited or new records would not be returned when using these named scopes. To work around this, I added the Enumerable select method to iterate over the line items, e.g.:

    # Reviewing shipments in an order
    order.line_items.select { |li| li.line_item_type == "shipment" }.all? { |s| s.shipment.status == "shipped" }
    
    # Get number of new payments
    order.line_items.select { |li| li.new_record? && li.line_item_type == "payment" }.size
    

    Association Extensions

    I felt that the above workaround was crufty and not very readable and sent out a request to my coworkers in hopes that there was a solution for improving the readability and clarity of the code. Kamil confirmed that named scopes do not return uncommitted records, and Tim Case offered an alternative solution by suggesting association extensions. An association extension allows you to add new finders, creators or methods that are only used as part of the association. After some investigation, I settled on the following code to extend the line_items association:

    class Piggybak::Order < ActiveRecord::Base
      has_many :line_items, do
        def sellables
          proxy_association.proxy.select { |li| li.ilne_item_type == "sellable" }
        end
        def taxes
          proxy_association.proxy.select { |li| li.ilne_item_type == "tax" }
        end
        def shipments
          proxy_association.proxy.select { |li| li.ilne_item_type == "shipment" }
        end
        def payments
          proxy_association.proxy.select { |li| li.ilne_item_type == "payment" }
        end
      end
    end
    

    The above code allows us to call order.line_items.sellables, order.line_items.taxes, order.line_items.shipments, and order.line_items.payments, which will return all new and existing line item records. These custom finder methods are used during order preprocessing which occurs during the ActiveRecord before_save callback before an order is finalized.

    Dynamic Creation

    Of course, the Piggybak code takes this a step further because additional custom line item types can be added to the code via Piggybak extensions (e.g. coupons, gift certificates, adjustments). To address this, association extensions are created dynamically in the Piggybak engine instantiation:

    Piggybak::Order.class_eval do
      has_many :line_items, do
        Piggybak.config.line_item_types.each do |k, v|
          # k is sellable, tax, shipment, payment, etc.
          define_method "#{k.to_s.pluralize}" do
            proxy_association.proxy.select { |li| li.line_item_type == "#{k}" }
          end
        end
      end
    end
    

    Conclusion

    The disadvantage to association extensions versus named scopes are that association extensions are not chainable, which means you cannot add methods to the association extension. For example, a named scope may allow you to query order.line_items.sellables.price_greater_than_50 to return committed line items with a price greater than 50, but this functionality would not be possible with association extensions. This is not a limitation in the current code base, but it may become a limitation in the future.

    ecommerce piggybak ruby rails


    Comments