riding rails for 10 years

50
Riding Rails for 10 Years John Duff

Upload: jduff

Post on 17-Jul-2015

197 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Riding rails for 10 years

Riding&Rails&for&10&YearsJohn%Duff

Page 2: Riding rails for 10 years

Overview• Why%look%at%Shopify?

• History%of%Rails%updates

• Hardest%things?

• Why%upgrade?

• Recommenda;ons

Page 3: Riding rails for 10 years

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

Page 4: Riding rails for 10 years
Page 5: Riding rails for 10 years
Page 6: Riding rails for 10 years

~45!different!versions!of!Rails!in!produc3on

Page 7: Riding rails for 10 years

Going&back&in&+me

Page 8: Riding rails for 10 years
Page 9: Riding rails for 10 years

Going&back&in&+me

• MVC%pa(erns%already%established

• Tes5ng%already%baked%in

• deploy.rb

Page 10: Riding rails for 10 years

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

Page 11: Riding rails for 10 years

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

Page 12: Riding rails for 10 years

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

Page 13: Riding rails for 10 years

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

Page 14: Riding rails for 10 years

Going&back&in&+me:&more

• Sweepers

• Observers

• rhtml/views

• Dependencies/in/vendor/or/submodules

• FastCGI/to/interface/with/the/webserver

Page 15: Riding rails for 10 years

--------------------------------------------------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--------------------------------------------------

Page 16: Riding rails for 10 years

A"lot"has"changed,"but"a"lot"is"s2ll"the"same

Page 17: Riding rails for 10 years

Rails&1.2

Page 18: Riding rails for 10 years

Rails&1.2

• REST&and&Resources

• Mul2byte&support

• Rou2ng&and&auto8loading&rewri;en

• Formats&and&respond_to

Page 19: Riding rails for 10 years

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

Page 20: Riding rails for 10 years

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

Page 21: Riding rails for 10 years

Rails&1.2:&more

• Ini%al(RESTful(urls(looked(like(/orders/1;edit

• Changed(in(Rails(1.2.4(to(/orders/1/edit

Page 22: Riding rails for 10 years

Rails&2.0

Page 23: Riding rails for 10 years

Rails&2.0

• Added%rescue_from

• Fixture%dependencies

• Rou5ng%namespaces

• Mul5%view%responses

• Ac5onWebService%out,%Ac5veResource%in

• JSON%serializa5on

Page 24: Riding rails for 10 years

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

Page 25: Riding rails for 10 years

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

Page 26: Riding rails for 10 years

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

Page 27: Riding rails for 10 years

Rails&2.1

Page 28: Riding rails for 10 years

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

Page 29: Riding rails for 10 years

Rails&2.3

Page 30: Riding rails for 10 years

Rails&2.3

• Rack

• accepts_nested_a-ributes

• find_in_batches

• improvements7to7Rails7Engines

• Rails7metal

• Applica<on7templates

Page 31: Riding rails for 10 years

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

Page 32: Riding rails for 10 years

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

Page 33: Riding rails for 10 years

Rails&3

Page 34: Riding rails for 10 years

Rails&3

• Arel

• Bundler

• Ac+ve-model-broken-up

• js/test/orm-agnos+c

Page 35: Riding rails for 10 years

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

Page 36: Riding rails for 10 years

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'

Page 37: Riding rails for 10 years

Rails&3.1&and&3.2

Page 38: Riding rails for 10 years

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

Page 39: Riding rails for 10 years

Rails&4.0

Page 40: Riding rails for 10 years

Rails&4.0

• Ruby&2

• Turbolinks

• Russian&doll&caching

• Strong&parameters

• Remove&observers

• Remove&hash&and&dynamic&finders

Page 41: Riding rails for 10 years

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')

Page 42: Riding rails for 10 years

Rails&4.1

Page 43: Riding rails for 10 years

Rails&4.1

• Spring(applica,on(preloader

• Variant(templates

Page 44: Riding rails for 10 years

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

Page 45: Riding rails for 10 years

That%brings%us%to%today

Page 46: Riding rails for 10 years

Started'easy,'got'hard

Page 47: Riding rails for 10 years

Hardest(things?• Marshaling+changes

• Maintaining+momentum+with+a+large+team

• Large+codebase+with+lots+of+edge+cases

• Performance+regressions

Page 48: Riding rails for 10 years

Why$upgrade?• New%features

• Be-er%security

• Hiring

• Codebase%longevity

Page 49: Riding rails for 10 years

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

Page 50: Riding rails for 10 years

[email protected]

@johnduff