riding rails for 10 years
TRANSCRIPT
Riding&Rails&for&10&YearsJohn%Duff
Overview• Why%look%at%Shopify?
• History%of%Rails%updates
• Hardest%things?
• Why%upgrade?
• Recommenda;ons
Why$look$at$Shopify?
• Started(around(the(same(/me(as(Rails
• Never(rewri6en
• Has(used(Rails(pre(1.0(to(4.2
• Git(repository(holds(most(of(this(history
~45!different!versions!of!Rails!in!produc3on
Going&back&in&+me
Going&back&in&+me
• MVC%pa(erns%already%established
• Tes5ng%already%baked%in
• deploy.rb
Going&back&in&+me:&rou+ng
ActionController::Routing::Routes.draw do |map| map.index '', :controller => 'shop'
# namespaces map.connect 'admin', :controller => 'admin/auth', :action => 'login' map.checkout 'checkout', :controller => 'checkout/standard', :action => 'index'
# media files map.connect 'media/:action/:id/:filename', :controller => 'media', :action => 'image'
# admin map.connect ':controller/:action/:id', :action => 'index', :id => nilend
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]) endend
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) endend
Going&back&in&+me:&javascript
module AjaxHelper def appear(id, where = nil) case where when :first "new Effect.Appear($('#{id}').firstChild)" else "new Effect.Appear('#{id}')" end endend
Going&back&in&+me:&more
• Sweepers
• Observers
• rhtml/views
• Dependencies/in/vendor/or/submodules
• FastCGI/to/interface/with/the/webserver
--------------------------------------------------Language files code--------------------------------------------------Ruby 177 4967Javascript 10 2436YAML 33 1465Ruby HTML 61 1399CSS 4 638SQL 2 573HTML 2 41Bourne Again Shell 1 2--------------------------------------------------SUM: 290 11521--------------------------------------------------
A"lot"has"changed,"but"a"lot"is"s2ll"the"same
Rails&1.2
Rails&1.2
• REST&and&Resources
• Mul2byte&support
• Rou2ng&and&auto8loading&rewri;en
• Formats&and&respond_to
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 endend
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 endend
Rails&1.2:&more
• Ini%al(RESTful(urls(looked(like(/orders/1;edit
• Changed(in(Rails(1.2.4(to(/orders/1/edit
Rails&2.0
Rails&2.0
• Added%rescue_from
• Fixture%dependencies
• Rou5ng%namespaces
• Mul5%view%responses
• Ac5onWebService%out,%Ac5veResource%in
• JSON%serializa5on
Rails&2.0:&rescue_from
# beforedef rescue_action(e) case e when MerchantCredentialError when IrreparableGoogleCheckoutError when ActiveRecord::RecordNotFound else endend
# afterrescue_from MerchantCredentialError do |exception| response.headers["WWW-Authenticate"] = %(Basic realm="Ping Backend") render :status => "401 Unauthorized"end
Rails&2.0:&Fixture&dependencies
# beforebigcheese_blog: id: 1 shop_id: 1 title: Mah Blog updated_at: 2006-02-02
# afterbigcheese_blog: shop: snowdevil title: Mah Blog updated_at: 2006-02-02
# Fixtures.identify(:snowdevil) when ambiguous
Rails&2.0:&Rou-ng&namespaces
# beforemap.resources :fulfillment_services, :path_prefix => 'admin/preferences', :controller => 'admin/preferences/fulfillment_services'
# afteradmin.namespace :admin do |admin| admin.namespace :preferences do |prefs| prefs.resources :fulfillment_services endend
Rails&2.1
Rails&2.1:&config.gems
# config/environment.rbRails::Initializer.run do |config| config.gem "right_aws" config.gem "entp-multipass", :source => 'http://gems.github.com'end
Rails&2.3
Rails&2.3
• Rack
• accepts_nested_a-ributes
• find_in_batches
• improvements7to7Rails7Engines
• Rails7metal
• Applica<on7templates
Rails&2.3:&Rack
# config/environment.rbActionController::Dispatcher.middleware.insert_before 'Rack::Lock', CommonBlacklist
# lib/common_blacklist.rbclass CommonBlacklist def initialize(app) @app = app end
def call(env) if env['REQUEST_URI'] =~ /^\/feedsplitter.php/ [404, {"Content-Type" => "text/plain"}, ['[Filtered]']] else @app.call(env) end endend
Rails&2.3:&accepts_nested_a2ributes
# beforeclass 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 shenanigansend
# afterclass ApiClient < ActiveRecord::Base attr_accessible :links_attributes
accepts_nested_attributes_for :links, :allow_destroy => trueend
Rails&3
Rails&3
• Arel
• Bundler
• Ac+ve-model-broken-up
• js/test/orm-agnos+c
Rails&3:&Arel
# beforeclass StoredAsset < ActiveRecord::Base def self.with_prefix(prefix) scoped(:conditions => {:prefix => prefix.to_s}) endend
# afterclass StoredAsset < ActiveRecord::Base scope :with_prefix, lambda { |prefix| where(:prefix => prefix.to_s) }end
Rails&3:&Bundler
# before# config/environment.rbRails::Initializer.run do |config| config.gem "right_aws"end
# after# Gemfilesource "http://gems.rubyforge.org"source "http://gems.github.com"gem "rack", '1.0.1'gem "rails", '2.3.5'
Rails&3.1&and&3.2
Rails&3.1&and&3.2
• Assets&pipeline
• JQuery&the&default&JS&library
• Lots&of&internal&API&changes
• 248&changed&files&with&1,366&addiAons&and&1,656&deleAons
Rails&4.0
Rails&4.0
• Ruby&2
• Turbolinks
• Russian&doll&caching
• Strong¶meters
• Remove&observers
• Remove&hash&and&dynamic&finders
Rails&4.0:&dynamic&finders
# beforeshop.customers.find_or_initialize_by_email(data[:email])PaymentProvider.find_all_by_type(:direct)
# aftershop.customers.where(data.slice(:email)).first_or_initializePaymentProvider.where(type: 'direct')
Rails&4.1
Rails&4.1
• Spring(applica,on(preloader
• Variant(templates
Rails&4.1:&Variant&templates
class ApplicationContoller < ActionController::Base before_action :set_mobile_variant
def set_mobile_variant request.variant = :mobile if request.user_agent =~ USER_AGENT_PATTERN endend
# views/admin/customers/show.html+mobile.erb
That%brings%us%to%today
Started'easy,'got'hard
Hardest(things?• Marshaling+changes
• Maintaining+momentum+with+a+large+team
• Large+codebase+with+lots+of+edge+cases
• Performance+regressions
Why$upgrade?• New%features
• Be-er%security
• Hiring
• Codebase%longevity
Recommenda)ons• Avoid'monkey'patching'Rails
• Keep'dependencies'low
• Ship'small'changes'early'and'o:en
• Parallel'CI
• Dedicate'a'team
• Ship'to'isolated'produc@on'servers
@johnduff