Ruby on Rails: The Good Parts



Ruby on Rails: The Good Parts

0 0


presentation-rails

Ruby on Rails presentation

On Github derwiki / presentation-rails

Ruby on Rails: The Good Parts

presented by

@derwiki

for AdRoll, May 5, 2015

What is Ruby?

  • A dynamic, object-oriented programming language developed in the mid-90's
  • Designed for programmer productivity and fun
  • Often people, especially computer engineers, focus on the machines. They think, "By doing this, the machine will run fast. By doing this, the machine will run more effectively. By doing this, the machine will something something something." They are focusing on machines. But in fact we need to focus on humans, on how humans care about doing programming or operating the application of the machines. We are the masters. They are the slaves.
    - Yukihiro Matsumoto, the creator of Ruby

What is Rails?

  • Full-stack framework written in Ruby
  • Shipped with OS X since 2007
  • Emphasizes the use of well-known software engineering patterns and paradigms:
  • Convention over configuration
  • Don't repeat yourself (DRY)
  • Active record pattern
  • Model–View–Controller (MVC)
  • Representational state transfer (REST)
  • ... and more

Why Rails?

  • Like Ruby, optimized for developer productivity
  • Which is why I roll my eyes when people complain that it's slow
  • CPU and memory is getting faster and cheaper, developers are already much more expensive and getting more expensive

Optimized... how?

  • Convention over configuration: once you learn "the Rails way", you know where everything is supposed to be and what the file is named -- cross project
  • Robust CLI tool for rote development chores
  • Once you learn this tool and the time savings it buys you, you start thinking how you can translate your feature
  • ActiveRecord is a mature feature that provides a predictable and terse way to query the database
  • Incorporates many best practices out-of-the-box

rails new PerfMon

  • Helper that sets up a new project skeleton
Gemfile
Gemfile.lock
README.rdoc
config.ru
Rakefile
app/
bin/
config/
db/
lib/
log/
public/
test/
tmp/
vendor/

Gemfile

source 'https://rubygems.org'
gem 'rails', '4.2.0'
gem 'sqlite3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
group :development, :test do
    gem 'byebug'
    gem 'web-console', '~> 2.0'
    gem 'spring'
end

app/

app/models/
app/controllers/application_controller.rb
app/views/layouts/application.html.erb
app/helpers/application_helper.rb
app/mailers/
app/assets/javascripts/application.js
app/assets/stylesheets/application.css
app/assets/images/
app/controllers/concerns/
app/models/concerns/

bin/ and config/

bin/bundle
bin/rails
bin/rake
bin/setup
config/application.rb
config/routes.rb
config/database.yml
config/environment.rb
config/environments/development.rb
config/environments/production.rb
config/environments/test.rb
config/locales/en.yml

config/initializers/

config/initializers/assets.rb
config/initializers/backtrace_silencers.rb
config/initializers/cookies_serializer.rb
config/initializers/filter_parameter_logging.rb
config/initializers/inflections.rb
config/initializers/mime_types.rb
config/initializers/session_store.rb
config/initializers/wrap_parameters.rb

db/, lib/, log/, public/, test/, tmp/, and vendor/

db/seeds.rb
lib/tasks/
lib/assets/
log/
public/{404,422,500}.html
public/favicon.ico
public/robots.txt
test/fixtures/
test/controllers/
test/mailers/
test/models/
test/helpers/
test/integration/
test/test_helper.rb
tmp/cache/
tmp/cache/assets/
vendor/assets/javascripts/
vendor/assets/stylesheets/

$ rails g scaffold Snapshot

$ rails g scaffold Snapshot requests:integer visual_complete:integer
db/migrate/20150504043737_create_snapshots.rb
app/models/snapshot.rb
test/models/snapshot_test.rb
app/controllers/snapshots_controller.rb
app/views/snapshots/
    {index edit show new _form}.html.haml
test/controllers/snapshots_controller_test.rb
app/helpers/snapshots_helper.rb
app/assets/javascripts/snapshots.coffee
app/assets/stylesheets/snapshots.scss
app/assets/stylesheets/scaffolds.scss

Update the database

$ rake db:migrate
== 20150504043737 CreateSnapshots: migrating ==================================
-- create_table(:snapshots)
  -> 0.0014s
== 20150504043737 CreateSnapshots: migrated (0.0015s) =========================

Verifying the migration

$ rails c
Loading development environment (Rails 4.2.0)
2.2.0 :001 > Snapshot.all
  Snapshot Load (1.6ms) SELECT "snapshots".* FROM "snapshots"
=> <ActiveRecord::Relation []>
2.2.0 :002 > Snapshot
  => Snapshot(id: integer, requests: integer, visual_complete: integer, created_at: datetime, updated_at: datetime)

config/routes.rb

Rails.application.routes.draw do
  resources :snapshots
  root: 'snapshots#index'
end

Enumerating routes

$ rake routes
Prefix Verb URI Pattern Controller#Action snapshots GET /snapshots(.:format) snapshots#index POST /snapshots(.:format) snapshots#create new_snapshot GET /snapshots/new(.:format) snapshots#new edit_snapshot GET /snapshots/:id/edit(.:format) snapshots#edit snapshot GET /snapshots/:id(.:format) snapshots#show PATCH /snapshots/:id(.:format) snapshots#update PUT /snapshots/:id(.:format) snapshots#update DELETE /snapshots/:id(.:format) snapshots#destroy

Controller

class SnapshotsController < ApplicationController
  before_action :set_snapshot,
              only: %i[show edit update destroy]
  def index # GET /snapshots
    @snapshots = Snapshot.all
  end
  def show # GET /snapshots/1
  end
  def new # GET /snapshots/new
    @snapshot = Snapshot.new
  end
  def edit # GET /snapshots/1/edit
  end

Controller

def create # POST /snapshots
  @snapshot = Snapshot.new(snapshot_params)
  respond_to do |format|
    if @snapshot.save
      format.html { redirect_to @snapshot, notice: 'Snapshot was successfully created.' }
      format.json { render :show, status: :created, location: @snapshot }
    else
      format.html { render :new }
      format.json { render json: @snapshot.errors }
    end
  end
end

Controller

def update # PATCH/PUT /snapshots/1
  respond_to do |format|
    if @snapshot.update(snapshot_params)
      format.html { redirect_to @snapshot, notice: 'Snapshot was successfully updated.' }
      format.json { render :show, status: :ok, location: @snapshot }
    else
      format.html { render :edit }
      format.json { render json: @snapshot.errors, status: :unprocessable_entity }
    end
  end
end

Controller

  def destroy # DELETE /snapshots/1
    @snapshot.destroy
    respond_to do |format|
      format.html { redirect_to snapshots_url, notice: 'Snapshot was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

Controller

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_snapshot
      @snapshot = Snapshot.find(params[:id])
    end
    # Never trust parameters from the scary internet, only allow the white list through.
    def snapshot_params
      params.require(:snapshot).permit(:requests, :visual_complete)
    end

Start the server

$ rails s -p 3003
=> Booting WEBrick
=> Rails 4.2.0 application starting in development on http://localhost:3003
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2015-05-03 21:44:39] INFO WEBrick 1.3.1
[2015-05-03 21:44:39] INFO ruby 2.2.0 (2014-12-25) [x86_64-darwin13]
[2015-05-03 21:44:39] INFO WEBrick::HTTPServer#start: pid=13475 port=3003

Request /snapshots

Started GET "/snapshots" for ::1 at 2015-05-03 21:47:24 -0700
Processing by SnapshotsController#index as HTML
Snapshot Load (0.1ms) SELECT "snapshots".* FROM "snapshots"
Rendered snapshots/index.html.haml within layouts/application (3.7ms)
Completed 200 OK in 1659ms (Views: 1655.5ms | ActiveRecord: 0.2ms)

/snapshots (index)

/snapshots (new)

/snapshots (show)

/snapshots (index)

Cron job

$ rails g task web_page_test run
     create  lib/tasks/web_page_test.rake
$ cat lib/tasks/web_page_test.rake
namespace :web_page_test do
  desc "TODO"
  task run: :environment do
  end
end

Rake task

require 'web_page_test'
namespace :web_page_test do
  desc "Run performance test"
  task run: :environment do
    requests, visual_complete =
      WebPageTest.test('http://example.com')
    PerfMon.create! requests: requests,
      visual_complete: visual_complete
  end
end

Heroku

$ heroku create perf-mon-example
Creating perf-mon-example... done, stack is cedar-14
https://perf-mon-example.herokuapp.com/ | https://git.heroku.com/perf-mon-example.git
Git remote heroku added
$ git remote -v
heroku https://git.heroku.com/perf-mon-example.git (fetch)
heroku https://git.heroku.com/perf-mon-example.git (push)

Pushing to Heroku

$ git push heroku master
Counting objects: 90, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (82/82), done.
Writing objects: 100% (90/90), 20.87 KiB | 0 bytes/s, done.
Total 90 (delta 5), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Ruby app detected
remote: -----> Compiling Ruby/Rails
remote: -----> Using Ruby version: ruby-2.0.0
remote: -----> Installing dependencies using 1.7.12

Pushing to Heroku

remote: Running: bundle install --without development:test
remote: Installing rails 4.2.0
remote: Your bundle is complete!
remote: Bundle completed (29.25s)
remote: Cleaning up the bundler cache.
remote: -----> Preparing app for Rails asset pipeline
remote: Running: rake assets:precompile
remote: Writing /tmp/build_62ee/public/assets/application-b598aa7.js
remote: Writing /tmp/build_62ee/public/assets/application-0723cb9.css
remote: Asset precompilation completed (7.66s)
remote: Cleaning assets
remote: Running: rake assets:clean

Pushing to Heroku

remote: -----> Discovering process types
remote: Procfile declares types -> (none)
remote: Default types for Ruby -> console, rake, web, worker
remote:
remote: -----> Compressing... done, 28.8MB
remote: -----> Launching... done, v6
remote: https://perf-mon-example.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/perf-mon-example.git
* [new branch] master -> master

Why Heroku

airportyogamats
anotherphotoproject
apartmentlist-referral-map
badpoop
boozysmoothies
buildingawebsiteonheroku
cameralends
cameralends-blog
cameralends-discourse
cameralends-new
cameralends-staging
classesnearme-prod
cookingproject
cupidsarrow
dailysitesnap
dereweckiphotography
derwiki-mockr
dss-screenshotter
enigmatic-hollows-7416
etsytools
facebooklite
fathersunion
formr
friendgrid
friendgridowski
goforcrew-beta
henrythedog
isduncansicktoday
jaspereng
johnhenryrails
just10cards
middleschoolscience
mockr-demo
moonshineonline
mynextmuni
openscience
openscience-legacy
openscience-staging
paidcodereview
paidcodereview-staging
perf-mon-example
putaframeonit
rails4payment
rails4payment-staging
rails4wizard
rails4wizard-staging
redface
retrotrakker
rollify-experimental
sample-project-123
seoranktracker
showoffyourmasterpiece
singlyhackathon
sitegazer
snowhio
subtwittle
verticalbrands-stars
web-page-test-adroll
web-page-test-cameralends
weblapse
whenissunset
whentogo
yetanotherproject

Setting up scheduler

$ heroku addons:add scheduler
Adding scheduler on perf-mon-example... done, v6 (free)
This add-on consumes dyno hours, which could impact your monthly bill. To learn more:
http://devcenter.heroku.com/addons_with_dyno_hour_usage
To manage scheduled jobs run:
heroku addons:open scheduler
Use `heroku addons:docs scheduler` to view documentation.
$ heroku addons:open scheduler
Opening scheduler:standard for perf-mon-example... done

Adding a new task

line_chart gem

= line_chart [{ name: 'Visually Complete', data: @chart }]
timedata = {}
@snapshots = Snapshot.order('id DESC').to_a.each do |ps|
  key = ps.created_at.strftime('%Y%m%d')
  timedata[key] ||= []
  timedata[key] << ps.visual_complete
end
@chart = []
timedata.each do |hour_time_key, values|
  average = values.inject(:+).to_f / values.size
  @chart << [Time.parse(hour_time_key), average]
end

line_chart gem

Asset pipeline

  • Framework for dealing with static assets (CSS, JS, images)
  • Concatenation and minification out-of-the-box
  • Fingerprinting, e.g. global-908e25f4bf641868d8683022a5b62f54.css
  • Advantage over time-based (index.css?1430782463): only bust cache when contents change
  • Adds level of pre-processing, e.g. index.css.scss.erb

ActiveRecord

  • Object Relational Mapper
  • Layer responsible for business data and logic
  • Support for associations via foreign key, inheritance, validation

ActiveRecord: (C)RUD

user = User.create!(name: "David",
                    occupation: "Code Artist")
or
user = User.new
user.name = "David"
user.occupation = "Code Artist"
user.save!

ActiveRecord: C(R)UD

users = User.where(occupation: "Code Artist")
            .order('id DESC')
user = User.where(occupation: "Code Artist")
           .first
user = User.find_by_email('user@example.com')
user = User.find(1)

ActiveRecord: CR(U)D

user = User.find(1)
user.name = 'David'
user.save!
or
user.update_attributes(name: 'David')
or
User.where(occupation: 'Code Artist')
    .update_all(occupation: 'Coder')

ActiveRecord: CRU(D)

User.find(1).destroy # triggers callbacks
User.find(1).delete # just deletes records
User.where(occupation: 'Code Artist')
    .delete_all

ActiveRecord: Single Table Inheritance

  • class MonopolyProperty < ActiveRecord::Base
  • class House < MonopolyProperty
  • class Hotel < MonopolyProperty
  • Uses `type` column
  • MonopolyProperty.where(type: 'house')
  • House.find(1)
  • # raises exception if 1 is a Hotel

ActiveRecord: Associations

  • class User < ActiveRecord::Base
      has_one :email
      # indicates `emails` table has `user_id`
      belongs_to :email
      # indicates this table has `email_id`
    end
  • User.find(1).email.address
  • User.where(occupation: 'Coder').each do |user|
      puts user.email.address
    end
  • N-query; each iteration hits `emails` table
  • Fix: User.includes(:email).where(occupation: 'Coder')
  • Collects all `id`s that match `where` and use them to select against `emails` eagerly
  • By default does not use join strategy

N-query gem

  • github.com/flyerhzm/bullet
  • gem install bullet
  • config.after_initialize do
      Bullet.enable = true
      Bullet.alert = true
      Bullet.console = true
    end
  • Available since 2009

ActiveRecord: Scopes

  • class User < ActiveRecord::Base
      scope :active, ->{ where 'confirmed_at IS NOT NULL' }
      scope :two_weeks, ->{ where 'created_at < ?', 2.weeks.ago }
    end
    User.active.two_weeks.count

Template Caching

  • Russian doll caching
  • e.g. Member directory by city
  • cache(['city', city.id]) do
      - @users.each do |user|
        - cache(['city_user', user.id]) do
          %h2= user.name
          %h2= user.email
  • - cache('home') do
      %h1 Welcome to Rails

Action Caching

  • class UsersController < ApplicationController
      caches_action :show,
            layout: true,
            cache_path: Proc.new { |c|
        { id: c.params[:user_id],
            is_owner: c.params[:user_id] == current_user.id
        } }
  • Action Caching cache hits serve in ~.1ms
  • Good caching strategies (efficient keys, pre-emptive caching) can drastically improve the performance of a web site
  • Biggest benefit if layout is cacheable; harder for logged-in users (AJAX user-tailored data strategy)

Action Caching in Action

Why Rails?

  • Ship faster
  • Ship more
  • Re-invent less wheels
  • Well-known vs other web frameworks

Ruby on Rails: The Good Parts

presented by

@derwiki

for AdRoll, May 5, 2015