This document provides an overview of the history of Rails updates experienced by Shopify over 10 years of using Rails. It describes some of the major Rails version updates Shopify went through, including Rails 1.2 which introduced REST and resources, Rails 2.0 which added rescue_from and fixture dependencies, and Rails 3 which split ActiveModel. It also discusses some of the hardest parts of maintaining a large codebase through Rails updates and recommends avoiding monkey patching Rails and shipping small changes frequently to upgrade more smoothly.
11. Going&back&in&+me:&controller
class Admin::OrdersController < AdminAreaController
def index
list
render :action => 'list'
end
def list
@pages = Paginator.new(self, shop.orders.count, 20, @params[:page])
@orders = shop.orders.find(:all, :limit => 20, :offset => @pages.current.offset)
end
def show
@order = shop.orders.find(@params[:id], :include => [:line_items, :payments])
end
end
12. Going&back&in&+me:&model
class Order < ActiveRecord::Base
belongs_to :shop
has_many :line_items
belongs_to :billing_address, :class_name => 'Address'
validates_presence_of :email, :shop
validates_format_of :email, :with => Format::EMAIL, :message => 'not a valid email'
serialize :receipt, Hash
attr_accessible :email
def deliver_confirmation_email
CheckoutMailer.deliver_user_confirmation(self)
end
end
19. Rails&1.2:&Formats&and&respond_to
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action.:format'
map.connect ':controller/:action/:id.:format'
end
class Admin::OrdersController < AdminAreaController
def list
@pages = Paginator.new(self, shop.orders.count(:all), 25, params[:page])
@orders = shop.orders.find(:all, :limit => 25, :offset => @pages.current.offset)
respond_to do |format|
format.html { render :action => 'list' }
format.csv { render_export_file('orders.csv', Mime::CSV, CSV.export(@orders)) }
end
end
end
20. Rails&1.2:&REST&and&Resources
ActionController::Routing::Routes.draw do |map|
map.resources :collects, :path_prefix => "admin", :controller => "admin/collects"
end
class Admin::CollectsController < AdminAreaController
def create
@collect = Collect.new(:product => @product, :collection => @collection)
@collect.save ? head(:created) : head(:precondition_failed)
end
def destroy
@collect = Collect.find(params[:id])
if @collect and shop.products.exists?(@collect.product_id)
@collect.destroy
head :ok
else
head :not_found
end
end
end
24. Rails&2.0:&rescue_from
# before
def rescue_action(e)
case e
when MerchantCredentialError
when IrreparableGoogleCheckoutError
when ActiveRecord::RecordNotFound
else
end
end
# after
rescue_from MerchantCredentialError do |exception|
response.headers["WWW-Authenticate"] = %(Basic realm="Ping Backend")
render :status => "401 Unauthorized"
end
32. Rails&2.3:&accepts_nested_a2ributes
# before
class ApiClient < ActiveRecord::Base
attr_accessible :new_link_attributes, :existing_link_attributes
def new_link_attributes=(link_attributes)
link_attributes.each do |attributes|
links.build(attributes)
end
end
# more shenanigans
end
# after
class ApiClient < ActiveRecord::Base
attr_accessible :links_attributes
accepts_nested_attributes_for :links, :allow_destroy => true
end
35. Rails&3:&Arel
# before
class StoredAsset < ActiveRecord::Base
def self.with_prefix(prefix)
scoped(:conditions => {:prefix => prefix.to_s})
end
end
# after
class StoredAsset < ActiveRecord::Base
scope :with_prefix, lambda { |prefix| where(:prefix => prefix.to_s) }
end