Skip to content

Instantly share code, notes, and snippets.

@outwitevil
Created February 5, 2016 15:43
Show Gist options
  • Save outwitevil/d6ca93f8b75d845f4acd to your computer and use it in GitHub Desktop.
Save outwitevil/d6ca93f8b75d845f4acd to your computer and use it in GitHub Desktop.
Rails guides #rails

http://guides.rubyonrails.org/getting_started.html

Rails Guides Getting Started Notes

rails new -h see all app builder switches

File/Folder Purpose

app/ Contains the controllers, models, views and assets for your application. You'll focus on this folder for the remainder of this guide.
config/ Configure your application's runtime rules, routes, database, and more. This is covered in more detail in Configuring Rails Applications
config.ru Rack configuration for Rack based servers used to start the application.
db/ Contains your current database schema, as well as the database migrations.
doc/ In-depth documentation for your application.
Gemfile Gemfile.lock These files allow you to specify what gem dependencies are needed for your Rails application.
lib/ Extended modules for your application.
log/ Application log files.
public/ The only folder seen to the world as-is. Contains the static files and compiled assets.
Rakefile This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.
README.rdoc This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.
script/ Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.
test/ Unit tests, fixtures, and other test apparatus. These are covered in Testing Rails Applications
tmp/ Temporary files
vendor/ A place for all third-party code. In a typical Rails application, this includes Ruby Gems, the Rails source code (if you optionally install it into your project) and plugins containing additional prepackaged functionality.

Databases

rails new blog --database=mysql this can also be used for overwriting

rake db:create creates dev and test db's

rake -T see all rake tasks

rails g controller home index

rm public/index.html

# config/routes.rb
Blog::Application.routes.draw do
  root :to => "home#index"

rails generate scaffold Post name:string title:string content:text

rake db:migrate RAILS_ENV=production don't need this if you're using Capistrano

Model: attr_accessible creates a whitelist for bulk updates (e.g. update_attributes)

rails c --sandbox rolls back any db changes made

reload! brings in changes to models into the running console

escaping in views by default <%= raw post.name %> for unescaped output

application layout app/views/layouts/application.html.erb

partials <%= render 'form' %> views/posts/_form.html.erb

Models

Most Rails developers still use generators to make things like models and controllers

Model generator rails g model Comment commenter:string body:text post:references

class Comment < ActiveRecord::Base
  belongs_to :post
end

# You’ll need to edit the post.rb file to add the other side of the association:

class Post < ActiveRecord::Base
  # ... validation etc. 
  has_many :comments
  # has_many :comments, :dependent => :destroy # cascade delete
end
class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :post
 
      t.timestamps
    end
 
    add_index :comments, :post_id
  end
end

Model name singular, table name plural

rails g model for help

Models inside modules rails generate model admin/account

Controllers

rails g controller Comments can be camel case or underscores, can also be modularised parent_module/controller_name

# adding a new comment to the comments associated with post
class CommentsController < ApplicationController
  def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.create(params[:comment])
    redirect_to post_path(@post)
  end
end
module PostsHelper
  def join_tags(post)
    post.tags.map { |t| t.name }.join(", ")
  end
end

Nested attributes e.g. tagging posts

rails generate model tag name:string post:references

accepts_nested_attributes_for :tags, :allow_destroy => :true, :reject_if => proc { |attrs| attrs.all? { |k, v| v.blank? } } goes in the model

see guide for forms

http://guides.rubyonrails.org/migrations.html

2. Migrations

API doc links

rake db:migrate also updates db/schema.rb to match structure of database

Examples

class CreateProducts < ActiveRecord::Migration
  def up
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps
    end
  end
 
  def down
    drop_table :products
  end
end
# pk id added by default
# created_at and updated_at also added by default

Migration to fix bad data:

class AddReceiveNewsletterToUsers < ActiveRecord::Migration
  def up
    change_table :users do |t|
      t.boolean :receive_newsletter, :default => false
    end
    User.update_all ["receive_newsletter = ?", true]
  end
 
  def down
    remove_column :users, :receive_newsletter
  end
end

Smart migration

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps
    end
  end
end

Other Migration methods

  • add_column
  • add_index
  • change_column
  • change_table
  • create_table
  • drop_table
  • remove_column
  • remove_index
  • rename_column

PostgreSQL can wrap migrations in a transaction, MySQL cannot

Location db/migrate

20080906120000_create_products.rb --> CreateProducts

20080906120001_add_details_to_products.rb --> AddDetailsToProducts.

Change a migration:

Only change recent uncommitted migrations, definitely not if already run in production.

Fix a recent mistake: db:rollback then alter the migration file and db:migrate

Data Types

  • :binary
  • :boolean
  • :date
  • :datetime
  • :decimal
  • :float
  • :integer
  • :primary_key
  • :string
  • :text
  • :time
  • :timestamp

Exotic Data Types (rdbms specific, non-portable)

create_table :products do |t|
  t.column :name, 'polygon', :null => false
end

Creating a Migration

For a new Model

rails generate model Product name:string description:text

gives us -->

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps
    end
  end
end

A blank migration

rails generate migration AddPartNumberToProducts

class AddPartNumberToProducts < ActiveRecord::Migration
  def change
  end
end

Add/Remove a column

rails generate migration AddPartNumberToProducts part_number:string

gives -->

class AddPartNumberToProducts < ActiveRecord::Migration
  def change
    add_column :products, :part_number, :string
  end
end

Multiple columns

rails generate migration AddDetailsToProducts part_number:string price:decimal

class AddDetailsToProducts < ActiveRecord::Migration
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

Writing a migration

Creating a table

# old style
create_table :products do |t|
  t.column :name, :string, :null => false
end

# new style
create_table :products do |t|
  t.string :name, :null => false
end

alternate primary key: create_table :products, :primary_key => "reg_number"

no primary key: create_table :products, :id => false

db specific: `create_table :products, :options => "ENGINE=MYISAM"

Changing tables

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end
# removes description and name, adds part_number, creates index on part_number, renames upccode

t.timestamps to add special created_at and updated_at columns to existing table

t.references :category creates a category_id column, model name passed, not column name, also available as belongs_to

Polymorphic reference

create_table :products do |t|
  t.references :attachment, :polymorphic => {:default => 'Photo'}
end
# creates cols attachment_id and attachment_type (with a default type of Photo)

Foreign keys have to be added through a call to execute or via a plugin

change Method

When using change instead of up and down the list of methods available is reduced:

  • add_column
  • add_index
  • add_timestamps
  • create_table
  • remove_timestamps
  • rename_column
  • rename_index
  • rename_table

up and down Method

for a complex migration down should be done in the reverse order of up

class ExampleMigration < ActiveRecord::Migration
  def up
    create_table :products do |t|
      t.references :category
    end
    #add a foreign key
    execute <<-SQL
      ALTER TABLE products
        ADD CONSTRAINT fk_products_categories
        FOREIGN KEY (category_id)
        REFERENCES categories(id)
    SQL
    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
 
  def down
    rename_column :users, :email_address, :email
    remove_column :users, :home_page_url
    execute <<-SQL
      ALTER TABLE products
        DROP FOREIGN KEY fk_products_categories
    SQL
    drop_table :products
  end
end

Irreversible

up ...delete loads of stuff

def down
  raise ActiveRecord::IrreversibleMigration
end

## Running Migrations

`rake db:migrate` also runs `rake db:schema:dump` this updates db/schema.rb

`rake db:migrate VERSION=20080906120000` migrates in either direction until this version is reached

`rake db:rollback`

`rake db:rollback STEP=3`

`rake db:migrate:redo` **useful trick to test the up and down of a new migration in one go**

`rake db:migrate:redo STEP=3`

`rake db:reset` drop database and load schema (via schema.rb not by running all migrations)

`say "Cheese"` debugging info inside a migration

## Using Models in migrations

Edge cases exist where using models in migrations can cause errors that prevent migrations working

Technique to safely use models in migrations (create a model that doesn't have validations etc etc)

````ruby
class AddFlagToProduct < ActiveRecord::Migration
  class Product < ActiveRecord::Base   # this bit is important
  end
 
  def change
    add_column :products, :flag, :integer
    Product.reset_column_information # this bit is important too
    Product.all.each do |product|
      product.update_attributes!(:flag => false)
    end
  end
end

Schema Dumping

db/schema.rb is the authoritative source for the database schema

Not for editing, represents current state of the database

No need to deploy by replaying hundreds of migrations (e.g. v1)

Test database is created by dumping dev db to schema.rb and then reloading it

schema.rb is the only place to see the current attributes for each model

:ruby (schema.rb) or :sql (structure.sql), hint use :sql

schema.rb doesn't support db specific stuff, (fk's, triggers, sprocs)

schema.rb cannot handle all of your execute statements

congig/application.rb --> config.active_record.schema_format = :sql

schema dumps should definitely be in source control

Foreign keys

Good luck ;)

use execute or a plugin like foreigner

http://guides.rubyonrails.org/active_record_validations_callbacks.html

Active Record Validations and Callbacks

p.new_record?

Following methods trigger validations. On invalid - bang: exception, save/updateattr: false, create/update: object

  • create
  • create!
  • save
  • save!
  • update
  • update_attributes
  • update_attributes!

Following skip validation:

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • touch
  • update_all
  • update_attribute
  • update_column
  • update_counters

person.valid? person.invalid? trigger validations and return true or false

If validations have been performed any errors will be accessible via person.errors

person.errors[:name] gets all errors for the name attribute but doesn't trigger validation

Validation Helpers

Acceptance

validates :terms_of_service, :acceptance => { :accept => 'yes' }

validates_associated

has_many :books; validates_associated :books; valid? will get called on all associated objects too (potential for infinite loop)

confirmation

validates :email, :confirmation => true type email address twice, virtual attribute created called email_confirmation

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

Confirmation requires a presence check too validates :email_confirmation, :presence => true otherwise a nil email_confirmation is allowed through

exclusion (or inclusion)

validates :subdomain, :exclusion => { :in => %w(www us ca jp), :message => "Subdomain %{value} is reserved." }

format

validates :legacy_code, :format => { :with => /\A[a-zA-Z]+\z/, :message => "Only letters allowed" }

length (or size)

class Person < ActiveRecord::Base
  validates :name, :length => { :minimum => 2 }
  validates :bio, :length => { :maximum => 500 }
  validates :registration_number, :length => { :is => 6, :wrong_length => 'should be 6 chars you know' }
  validates :password, :length => { 
    :in => 6..20,
    :too_long => "password is too long, you used %{count} characters",
    :too_short => 'too short'
  }
end

#need to count words, use the tokenizer option. :tokenizer => lambda { |str| str.scan(/\w+/) },

numericality

validates :points, :numericality => true validates :games_played, :numericality => {:only_integer => true} :greater_than, :greater_than_or_equal_to, :less_than, :less_than_or_equal_to

presence

uses blank? to for nil or empty string (all whitespace still fails)

validates :name, :login, :email, :presence => true 3 in one

belongs_to :order; validates :order_id, :presence => true validate presence of associated object

uniqueness

** does not use indexes, these must be created seperately**

validates :email, :uniqueness => true this generates a db query to check if value exists during validation

validates :name, :uniqueness => { :scope => :year, :message => "should happen once per year" }

validates_with

Super charged validations

class Person < ActiveRecord::Base
  validates_with GoodnessValidator
end
 
class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if record.first_name == "Evil"
      record.errors[:base] << "This person is evil"
    end
  end
end
# record.errors[:base] relates to record as a whole
# errors must be manually added as above

validates_each

class Person < ActiveRecord::Base
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
  end
end

Common Validation Options

:allow_nil => true e.g. value must be in a discrete list or nil

:allow_blank => true e.g. length must be at least 5 or blank

:message override the error message

:on => :create | :on => :update | :on => :save

Conditional Validation

via eval string or defined method

class Order < ActiveRecord::Base
  validates :card_number, :presence => true, :if => :paid_with_card?
 
  def paid_with_card?
    payment_type == "card"
  end
end
# possible to do :if => "payment_type == \"card\""; but not very nice

or via Proc

class Account < ActiveRecord::Base
  validates :password, :confirmation => true,
    :unless => Proc.new { |a| a.password.blank? }
end

Group conditional validations

class User < ActiveRecord::Base
  with_options :if => :is_admin? do |admin|
    admin.validates :password, :length => { :minimum => 10 }
    admin.validates :email, :presence => true
  end
end

Custom Validations

re-usability

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "is not an email")
    end
  end
end
 
class Person < ActiveRecord::Base
  validates :email, :presence => true, :email => true
end

custom methods

class Invoice < ActiveRecord::Base
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value
 
  def expiration_date_cannot_be_in_the_past
    if !expiration_date.blank? and expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end
 
  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end

add to ActiveRecord::Base

ultimate in easy re-use

ActiveRecord::Base.class_eval do
  def self.validates_as_choice(attr_name, n, options={})
    validates attr_name, :inclusion => { { :in => 1..n }.merge!(options) }
  end
end
class Movie < ActiveRecord::Base
  validates_as_choice :rating, 5
end

Working with errors

person.errors returns an errors object that behaves like a hash

person.errors[] returns array of strings of all errors for the attribute, no errors means empty array

person.errors.add(:name, 'name cannot be Dave')

person.errors[:base] << "this is invalid because ..." an error for object as a whole

errors.clear

errors.size

Displaying Validation errors

f.error_messages or error_messages_for :person

Plenty of ways to customise error messages displayed

Use simple_form

Call backs

Tend to violate SRP, e.g. after_create => :send_email

Not very good for testability and maintainability

before_validation :do_something or before_create do |user| ... end

Many callbacks exist related to creating, updating, finding and initializing

Be careful as several methods interact with models but do not trigger callbacks, if you find the need for callbacks look into this more.

Callbacks can be triggered by associated models :dependent: destroy

Callbacks can be conditional , :if => :paid_with_card?

:if, :unless etc can accept a symbol, string or proc

Callbacks can defined as classes to allow re-use

Observers

Alternative to callbacks that don't pollute Models with unrelated code

rails generate observer User --> app/models/user_observer.rb

class UserObserver < ActiveRecord::Observer
  def after_create(model)
    # code to send confirmation email...
  end
end

Registering observers

config/application.rb config.active_record_observers = :user_observer

or delete it from application.rb and add to config/environments/production.rb only

Sharing observers

explicitly link an observer to Models

class MailerObserver < ActiveRecord::Observer
  observe :registration, :user
 
  def after_create(model)
    # code to send confirmation email...
  end
end

Transation callbacks

after_commit and after_rollback

used when interacting with external systems, file system, network (e.g. don't delete a file until you're sure the record was deleted successfully)

http://guides.rubyonrails.org/association_basics.html

Associations basics

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

belongs_to (one to one)

Submissions belong to Trainees

class Order < ActiveRecord::Base
  belongs_to :customer
end

has_one (one to one)

Like a belongs_to but from navigating from the other direction

class Supplier < ActiveRecord::Base
  has_one :account
end

has_many (one to many)

Often on the other side of a belongs_to

# pluralize the model name for a has_many
class Customer < ActiveRecord::Base
  has_many :orders
end

has_many :through (many to many)

A many to many where you will need to work with the joiner in its own right

If the relationship Model needs validations / callbacks / extra attributes use :through

class Physician < ActiveRecord::Base
  has_many :appointments
  has_many :patients, :through => :appointments
end
 
class Appointment < ActiveRecord::Base
  belongs_to :physician
  belongs_to :patient
end
 
class Patient < ActiveRecord::Base
  has_many :appointments
  has_many :physicians, :through => :appointments
end

physician.patients
patients.physicians = patients # creates new join models where necessary, deletes others (destroy callbacks not called)

Mutli levelled has_many :through

class Document < ActiveRecord::Base
  has_many :sections
  has_many :paragraphs, :through => :sections
end
 
class Section < ActiveRecord::Base
  belongs_to :document
  has_many :paragraphs
end
 
class Paragraph < ActiveRecord::Base
  belongs_to :section
end
@document.paragraphs works as exected

has_one :through

Really?

has_and_belongs_to_many

many to many without an intervening Model (but still a basic table)

class Assembly < ActiveRecord::Base
  has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
  has_and_belongs_to_many :assemblies
end

Setting up a one to one

belongs_to for one has_one for another, how do you know which one

Polymorphic Associations

See example to allow a Picture Model to belong to either an employee or a product

Self Joins

see example

Caching

Results are cached unless you force them not to be

customer.orders; customer.orders.size; customer.orders(true).size

Naming

Avoid names that will overwrite existing Model methods

Schema

Its up to you

order belongs_to customer: migration needs a t.integer :customer_id (or t.references :customer ?) for a new model, or add_column for existing model

habtm you need to do a migration to make the join table

class CreateAssemblyPartJoinTable < ActiveRecord::Migration
  def change
    create_table :assemblies_parts, :id => false do |t|
      t.integer :assembly_id
      t.integer :part_id
    end
  end
end
# :id => false is used as this is not a "Model"

inverse of

Makes active record aware that this relationship goes both ways.

class Customer < ActiveRecord::Base
  has_many :orders, :inverse_of => :customer
end
 
class Order < ActiveRecord::Base
  belongs_to :customer, :inverse_of => :orders
end

Further detail

belongs_to

order belongs_to customer

  • order.customer pass in true to force reload
  • order.customer=
  • order.build_customer pass in a hash of values to build associated object, but don't save
  • order.create_customer as build_ but save as well

** options for belongs_to. e.g. belongs_to :customer, :conditions => "active = 1"**

  • :autosave save / destroy as soon as parent is saved
  • :class_name when the Model name is different to the association name
  • :conditions sql used to limit which objects can be associated
  • :counter_cache keep a count of the size of the collection in an attribute of the containing model
  • :dependent set to :destroy to call the destroy method on all associated objects
  • :foreign_key set a non-standard fk attribute name
  • :include eager loading, only necessary for non-immediate associations (3 levels lineitem-order-customer)
  • :inverse_of bi-directional see above
  • :polymorphic
  • :readonly prevent editing of associated objects
  • :select override the sql used
  • :touch set to true and all associated objects will have their created_at etc updated when parent is saved
  • :validate set to true to trigger validation on all associated objects when parent is saved

** Eager loading example **

class LineItem < ActiveRecord::Base
  belongs_to :order, :include => :customer
end
 
class Order < ActiveRecord::Base
  belongs_to :customer
  has_many :line_items
end
 
class Customer < ActiveRecord::Base
  has_many :orders
end

Check if any associated objects exist @order.customer.nil?

has_many

class Customer < ActiveRecord::Base
  has_many :orders
end

**Each instance of the customer model will have these methods:**

orders(force_reload = false)
orders<<(object, ...)
orders.delete(object, ...)
orders=objects
order_ids
order_ids=ids (adds/deletes to make collection match the supplied ids)
orders.clear
orders.empty?
orders.size
orders.find(...) (as per ActiveRecord::Base.find)
orders.where(...) (creates a relation object, lazy loaded, only queries when data is accessed, e.g. call .first)
orders.exists?(...)
orders.build(attributes = {}, ...) no save
orders.create(attributes = {}) save

:conditions

class Customer < ActiveRecord::Base
  has_many :confirmed_orders, :class_name => "Order",
    :conditions => "confirmed = 1"
end
# procs for dynamic evaluation
class Customer < ActiveRecord::Base
  has_many :latest_orders, :class_name => "Order",
    :conditions => proc { ["orders.created_at > ?", 10.hours.ago] }
end

:dependent

??? not sure about this stuff

:dependent => :destroy null the fk and call destroy on the object

:dependent => :delete_all null the fk and call delete on the object

When saved

as soon as they're added to a collection they're saved in order to update their foreign key

assignment returns false if saving fails due to validation errors

if parent is not saved then children are not saved until parent is saved

assign an object without saving by using the build method

has_and_belongs_to_many

assemblies(force_reload = false)
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies=objects
assembly_ids
assembly_ids=ids
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})

options

many options similar to has_many

:association_foreign_key

# using unconventional names for the habtm
class User < ActiveRecord::Base
  has_and_belongs_to_many :friends, :class_name => "User",
    :foreign_key => "this_user_id",
    :association_foreign_key => "other_user_id"
end

When saved

Saved as soon as they're added to an association.

http://guides.rubyonrails.org/active_record_querying.html

Active Record Query Interface

class Client < ActiveRecord::Base
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end
class Address < ActiveRecord::Base
  belongs_to :client
end
class Order < ActiveRecord::Base
  belongs_to :client, :counter_cache => true
end
class Role < ActiveRecord::Base
  has_and_belongs_to_many :clients
end

All these methods return an ActiveRecord::Relation

  • where
  • select
  • group
  • order
  • reorder
  • reverse_order
  • limit
  • offset
  • joins
  • includes
  • lock
  • readonly
  • from
  • having

Client.find(10) Client.first Client.first! raise RecordNotFound

Client.find([2,5,7])

Batches, .all is not a great idea

batch processing uses find_each and find_in_batches

looping 1000 records is fine to use normal .each, if memory is an issue use find_each and find_in_batches

Conditions

Client.where("orders_count = ?", params[:orders])

Named params: Client.where("created_at >= :start_date AND created_at <= :end_date", {:start_date => params[:start_date], :end_date => params[:end_date]})

Range: Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date))

Hash conditions: Client.where(:locked => true)

Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date))

Only equality, range and subset checking are possible with Hash conditions

In: Client.where(:orders_count => [1,3,5])

Order: Client.order("orders_count ASC, created_at DESC")

Select specific fields (returns read only objects)

Client.select("viewable_by, locked")

select distinct: Client.select(:name).uniq

Client.limit(5).offset(30)

Group by

Total sales per day: Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)").having("sum(price) > ?", 100)

Overriding

.except(:order) removes the order by

.only(:order, :where) removes all but order and where

.reorder('name') overrides an order set by a default scope e.g. has_many :comments, :order => 'posted_at DESC'

.reverse_order

Locking

Optimistic locking requires table to have a column called lock_version

Pessimistic locking call Item.lock.first

Joining Tables

Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')

Category.joins(:posts)

Post.joins(:comments => :guest)

Multi-level joins Category.joins(:posts => [{:comments => :guest}, :tags])

SELECT categories.* FROM categories
  INNER JOIN posts ON posts.category_id = categories.id
  INNER JOIN comments ON comments.post_id = posts.id
  INNER JOIN guests ON guests.comment_id = comments.id
  INNER JOIN tags ON tags.post_id = posts.id

**Add conditions to your joins

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)

# hash style
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(:orders => {:created_at => time_range})

Eager loading .includes(:address)

Active Record lets you specify in advance all the associations that are going to be loaded.

clients = Client.includes(:address).limit(10)
 
clients.each do |client|
  puts client.address.postcode
end

Advanced eager loading

Post.includes(:category, :comments) multiple associations eager loaded

Category.includes(:posts => [{:comments => :guest}, :tags]).find(1)

Post.includes(:comments).where("comments.visible", true) don't do this, use joins Post.joins(:comments).where('comments.visible' => true)

Scopes

Commonly used ARel queries

class Post < ActiveRecord::Base
  scope :published, where(:published => true)
end

scope :published, where(:published => true).joins(:category)

scope :published, where(:published => true)
scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))

using scopes

Post.published directly on class or category = Category.first; category.posts.published; on an association

scope :last_week, lambda { where("created_at < ?", Time.zone.now ) } use lambda for times to delay evaluation

scope arguments

this can be done with a lambda that accepts an argument but the preferred technique is to use a simple class method instead.

adding scopes on the fly

client = Client.find_by_first_name("Ryan")
orders = client.orders.scoped
... then much later
orders.where('created at > ?', 30.days.ago)

default scope

affects all queries on the model

default_scope where("removed_at is null")

remove all scoping

Client.unscoped.all

##Find by SQL

Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER clients.created_at desc")
# this gives you objects
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
# this gives you good old fashioned array of hashes

Pluck

Client.where(:active => true).pluck(:id) no need to use map anymore

##Existence

Client.exists?(1)

Client.where(:first_name => 'Ryan').exists? bit more sophisticated

.any? and .many? can also be used on Class or relation

Calculations

Client.count Client.average("orders_count") Client.minimum(:age) Client.maximum(:age) Client.sum("orders_count")`

Explain

User.where(:id => 1).joins(:posts).explain

Auto explain: config.active_record.auto_explain_threshold_in_seconds

http://guides.rubyonrails.org/layouts_and_rendering.html

Layouts and Rendering

Creating a response

render explicitly send full response to browser

redirect_to send http redirect status to browser

head send only http headers

Default rendering

class BooksController... + resources :books + app/views/books/index.html.erb

render :nothing might be used to indicate a successful ajax response. using head is clearer though

render :edit render an alternative view within the controllers view path

render :action => :edit does same thing N.B action is not run, simply the view is used

render 'products/show' use a view from another controller. Same as render :template => 'products/show'

# so many ways to render a view
render :edit
render :action => :edit
render 'edit'
render 'edit.html.erb'
render :action => 'edit'
render :action => 'edit.html.erb'
render 'books/edit'
render 'books/edit.html.erb'
render :template => 'books/edit'
render :template => 'books/edit.html.erb'
render '/path/to/rails/app/views/books/edit'
render '/path/to/rails/app/views/books/edit.html.erb'
render :file => '/path/to/rails/app/views/books/edit'
render :file => '/path/to/rails/app/views/books/edit.html.erb'

render :text => 'hello world' also omits the layout

render :json => @product or :xml, to_json and to_xml are called for you

other render options

  • :content_type e.g. 'application/rss'
  • :layout e.g. 'special_layout' or false
  • :status e.g. 500 or forbidden
  • :location sets http location header

Layouts

  • can be controller specific app/views/layouts/photos.html.erb
  • can be app wide app/views/layouts/application.html.erb
  • can be specified in the controller layout "inventory" PhotosController
  • can be specified for whole app ApplicationController layout "main"
  • use a symbol to represent a method to allow dynamic layout selection e.g. layout :random_layout_picker
  • can also pass Proc to layout, Proc receives the controller as a parameter
  • conditional layouts layout "product", :except => [:index, :rss]

Double render error

If you're rendering different things inside conditional branches then remember to add and return to prevent double render

&& will not work due to operator precedence, apparently.

implicit render is smart enough never to do a double render even if you haven't and return'd

redirect_to

Tell the browser to start a completely new response

Same args as a link_to or url_for

Structuring Layouts

Asset Tag Helpers

  • auto_discovery_link_tag
  • javascript_include_tag
  • stylesheet_link_tag
  • image_tag
  • video_tag
  • audio_tag

<%= image_tag "home.gif", :alt => "Home" %>

yield

<%= yield %> or <%= yield :leftnav %> allows you to specify later <% content for :leftnav do %>hi<% end %>

Partials

render 'menu' will render _menu.html

render 'shared/menu' will render app/views/shared/_menu.html.erb

render :partial => 'great_links', :layout => 'graybox' layouts for partials also use underscore prefix, they live with the partial not the layouts

Passing local variables in to partials

#new.html.erb
<h1>New zone</h1>
<%= error_messages_for :zone %>
<%= render :partial => "form", :locals => { :zone => @zone } %>

#edit.html.erb
<h1>Editing zone</h1>
<%= error_messages_for :zone %>
<%= render :partial => "form", :locals => { :zone => @zone } %>

#_form.html.erb
<%= form_for(zone) do |f| %>
  <p>
    <b>Zone name</b><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

** conventional

<%= render :partial => "customer", :object => @new_customer %> every partial has access to a local variable name the same as the partial, this can be set by the :object option

if its a regular model you can even do

<%= render @customer %> and rails will figure out the details

Partials and collections, conventions

<%= render :partial => 'product', :collection => @products %> rails loops collection for you and passes each value to the partial

partial can them be as simple as <p>Product Name: <%= product.name %></p>

<%= render @products || 'No products found.' %> ultra conventional, returns nil if empty collection

:as option can be used to change the name of the local variable used by the collection oriented partial

you can also still pass in further variable via :locals => {:a => 'a', :b => 'b'}

:spacer_template => 'product_ruler' neatly define what goes inbetween each rendering of the collection items

Nested layouts / sub-templates

Flexible stylesheets and content

#app/views/layouts/application.html.erb
<html>
<head>
  <title><%= @page_title or 'Page Title' %></title>
  <%= stylesheet_link_tag 'layout' %>
  <style type="text/css"><%= yield :stylesheets %></style>
</head>
<body>
  <div id="top_menu">Top menu items here</div>
  <div id="menu">Menu items here</div>
  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
</body>
</html>
#app/views/layouts/news.html.erb
<% content_for :stylesheets do %>
  #top_menu {display: none}
  #right_menu {float: right; background-color: yellow; color: black}
<% end %>
<% content_for :content do %>
  <div id="right_menu">Right menu items here</div>
  <%= content_for?(:news_content) ? yield(:news_content) : yield %>
<% end %>
<%= render :template => 'layouts/application' %>

http://guides.rubyonrails.org/form_helpers.html

Form Helpers

<%= form_tag do %>Form contents<% end %>

<%= form_tag("/search", :method => "get") do %>
  <%= label_tag(:q, "Search for:") %>
  <%= text_field_tag(:q) %>
  <%= submit_tag("Search") %>
<% end %>

multiple hashes in a form_tag

form_tag({:controller => "people", :action => "search"}, :method => "get", :class => "nifty_form")

<%= text_area_tag(:message, "Hi, nice site", :size => "24x6") %>
<%= password_field_tag(:password) %>
<%= hidden_field_tag(:parent_id, "5") %>
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= url_field(:user, :homepage) %>
<%= email_field(:user, :address) %>

<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
<input id="password" name="password" type="password" />
<input id="parent_id" name="parent_id" type="hidden" value="5" />
<input id="user_name" name="user[name]" size="30" type="search" />
<input id="user_phone" name="user[phone]" size="30" type="tel" />
<input id="user_homepage" size="30" name="user[homepage]" type="url" />
<input id="user_address" size="30" name="user[address]" type="email" />


<%= check_box_tag(:pet_dog) %>
<%= label_tag(:pet_dog, "I own a dog") %>
<%= check_box_tag(:pet_cat) %>
<%= label_tag(:pet_cat, "I own a cat") %>

<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
<label for="pet_dog">I own a dog</label>
<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
<label for="pet_cat">I own a cat</label>

<%= radio_button_tag(:age, "child") %>
<%= label_tag(:age_child, "I am younger than 21") %>
<%= radio_button_tag(:age, "adult") %>
<%= label_tag(:age_adult, "I'm over 21") %>

<input id="age_child" name="age" type="radio" value="child" />
<label for="age_child">I am younger than 21</label>
<input id="age_adult" name="age" type="radio" value="adult" />
<label for="age_adult">I'm over 21</label>

Forms and Models

<%= text_field(:person, :name) %>

<input id="person_name" name="person[name]" type="text" value="Henry"/>

accessible via params[:person][:name]

Person.new params[:person]

@person.update_attributes params[:person]

<%= text_field(:person, :floogle) %> so long as person has a floogle and a floogle= method this will work (even if its not a db direct mapping)

Bind form to object / model

A model with many fields would be verbose to write the above

Controller new action: @article = Article.new

<%= form_for @article, :url => { :action => "create" }, :html => {:class => "nifty_form"} do |f| %>
  <%= f.text_field :title %>
  <%= f.text_area :body, :size => "60x12" %>
  <%= f.submit "Create" %>
<% end %>

<form accept-charset="UTF-8" action="/articles/create" method="post" class="nifty_form">
  <input id="article_title" name="article[title]" size="30" type="text" />
  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
  <input name="commit" type="submit" value="Create" />
</form>

# f is yielded, this is a form builder object

:namespace option is available to easily keep things unique

sub-form for associated model

<%= form_for @person, :url => { :action => "create" } do |person_form| %>
  <%= person_form.text_field :name %>
  <%= fields_for @person.contact_detail do |contact_details_form| %>
    <%= contact_details_form.text_field :phone_number %>
  <% end %>
<% end %>

<form accept-charset="UTF-8" action="/people/create" class="new_person" id="new_person" method="post">
  <input id="person_name" name="person[name]" size="30" type="text" />
  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" size="30" type="text" />
</form>

Getting more Conventional (Record Identification)

#routes.rb
resources :articles

## Creating a new article
# long-style:
form_for(@article, :url => articles_path)
# same thing, short-style (record identification gets used):
form_for(@article)
 
## Editing an existing article
# long-style:
form_for(@article, :url => article_path(@article), :html => { :method => "put" })
# short-style:
form_for(@article)

namespaced application: form_for [:admin, @article]

Select and Option

<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %> not much use to anyone

<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %> pass in selected value 2 ("2" would not work here)

<%= select_tag(:city_id, options_for_select(...), 2) %> put it all together

Select and Models

<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %> selected val is already known from the model

with a form builder

<%= f.select(:city_id, ...) %>

Option tags from collections

<% cities_array = City.all.map { |city| [city.name, city.id] } %>
<%= options_for_select(cities_array) %>

# or shorter version:

<%= options_from_collection_for_select(City.all, :id, :name) %>

# bring them all together

<%= collection_select(:person, :city_id, City.all, :id, :name) %>

FormBuilders

for powerful customisation

Arrays in forms

person[phone_number][] when rails sees multiple with same name it starts up an array

addresses[][line1], addresses[][line2] creates an array of hashes

check_box helper doesn't work with arrays as it uses a hidden input with the same name to make check boxes always submit a value

rails uses duplicate names as the cue to start up an array so this can end in much confusion

Nested form for Person with many addresses

<%= form_for @person do |person_form| %>
  <%= person_form.text_field :name %>
  <% @person.addresses.each do |address| %>
    <%= person_form.fields_for address, :index => address do |address_form|%>
      <%= address_form.text_field :city %>
    <% end %>
  <% end %>
<% end %>

<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post">
  <input id="person_name" name="person[name]" size="30" type="text" />
  <input id="person_address_23_city" name="person[address][23][city]" size="30" type="text" />
  <input id="person_address_45_city" name="person[address][45][city]" size="30" type="text" />
</form>
#{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}

<%= fields_for 'person[address][]', address do |address_form| %> don't have to specify the index

FIDDLY STUFF :)

http://guides.rubyonrails.org/action_controller_overview.html

Controllers

wired up via routes

Parameters

get and post treated equally: params hash

passing arrays GET /clients?ids[]=1&ids[]=2&ids[]=3 params[:ids] will be ["1", "2", "3"]

passing hashes: params[:client][:address]

<form accept-charset="UTF-8" action="/clients" method="post">
  <input type="text" name="client[name]" value="Acme" />
  <input type="text" name="client[phone]" value="12345" />
  <input type="text" name="client[address][postcode]" value="12345" />
  <input type="text" name="client[address][city]" value="Carrot City" />
</form>

params hash is a HashWithIndifferenetAccess, basically a hash that doesn't care if key is string or symbol

JSON and XML in parameters

JSON and XML will be detected in a paramater and automatically converted to a ruby hash

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

You’ll get params[:company] as { :name => “acme”, “address” => “123 Carrot Street” }

root element can be omitted from JSON if you use config.wrap_parameters = true

Routing Parameters

:controller and :action keys are always present but use methods controller_name and action_name

match '/clients/:status' => 'clients#index', :foo => 'bar'

GET /clients/active will give params = {:status => "active", :foo => "bar"}

default_url_options

e.g. add this method to ApplicationController to pass in a localisation parameter

Session

Choose between simple cookie or database backed session

Simple is recommended. Cookie is tamper proof but is readable on the users machine. Limit of 4kb

DB session store

config/initializers/session_store.rb YourApp::Application.config.session_store :active_record_store

create the session table with script/rails g session_migration

configure the cookie key and domain

YourApp::Application.config.session_store :cookie_store, :key => '_your_app_session', :domain => ".example.com"

config/initializers/secret_token.rb must be long and random

YourApp::Application.config.secret_token = '49d3f3de9ed86c74b94ad6bd0...' chamge this and all sessions are invalidated

Session example for user authentication

class ApplicationController < ActionController::Base
 
  private
 
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by_id(session[:current_user_id])
  end
end
class LoginsController < ApplicationController
  def create
    if user = User.authenticate(params[:username], params[:password])
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end

  def destroy
    @_current_user = session[:current_user_id] = nil
    redirect_to root_url
  end
end

reset_session resets the entire session probably a good idea on logout

Flash

inside an action flash[:notice] = "You have successfully logged out"

as part of a redirection redirect_to root_url, :notice => "You have successfully logged out"

Display flash: might have a catch all flash displayer in the application layout: <% if flash[:notice|:error] %><%= flash[:notice|:error] %><% end %>`

flash.keep persist over the flash, flash.now useful for a call to render rather than redirect

Cookies

Behaves a lot like flash, deleting is done by cookies.delete[:key]

Filters

before_filter e.g. in ApplicationController before_filter :require_login

class ApplicationController < ActionController::Base
  before_filter :require_login
 
  private
 
  def require_login
    unless logged_in?
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url # halts request cycle
    end
  end
 
  def logged_in?
    !!current_user # force conversion to boolean
  end
end

disable the filter for the actual login actions

class LoginsController < ApplicationController
  skip_before_filter :require_login, :only => [:new, :create]
end

instead of private methods symbolized, filters can also be applied using a block or a full class (with a filter method)

Request Forgery Protection

handled automatically if using common form helpers.

For manually created forms remember to call form_authenticity_token to insert the authenticity_token

Request object

access to lower level stuff about the request

Response object

access to lower level stuff about the response

can be used to set custom headers response.headers["Content-Type"] = "application/pdf"

Streaming Files

send_data send_file latter is convenience version

# generate and send a pdf
require "prawn"
class ClientsController < ApplicationController
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              :filename => "#{client.name}.pdf",
              :type => "application/pdf"
  end
 
  private
 
  def generate_pdf(client)
    Prawn::Document.new do
      text client.name, :align => :center
      text "Address: #{client.address}"
      text "Email: #{client.email}"
    end.render
  end
end
# set :disposition => 'inline' if the file is not a "download" e.g. display an image in the browser
# headers must be set by default to make it act like a "download"
# send file from file system
class ClientsController < ApplicationController
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              :filename => "#{client.name}.pdf",
              :type => "application/pdf")
  end
end
# :type can be omitted, file extension allows guessing, application/octet-stream used if unknown file extension

not for static files, these go in the public folder

Restful downloads

class ClientsController < ApplicationController
  # The user can request to receive this resource as HTML or PDF.
  def show
    @client = Client.find(params[:id])
 
    respond_to do |format|
      format.html
      format.pdf { render :pdf => generate_pdf(@client) }
    end
  end
end
# config/initializers/mime_types.rb needs to have this added: Mime::Type.register "application/pdf", :pdf
# GET /clients/1.pdf now gives neat pdf download

Parameter filtering

config.filter_parameters << :password << :confirm_password << :favourite_song then these all show as [FILTERED]

Rescue from Errors and Exceptions

Production 404.html and 500.html

Using custome Exceptions to help with authorisation

Quite nifty, send them back a page and show flash message

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, :with => :user_not_authorized
 
  private
 
  def user_not_authorized
    flash[:error] = "You don't have access to this section."
    redirect_to :back
  end
end
 
class ClientsController < ApplicationController
  # Check that the user has the right authorization to access clients.
  before_filter :check_authorization
 
  # Note how the actions don't have to worry about all the auth stuff.
  def edit
    @client = Client.find(params[:id])
  end
 
  private
 
  # If the user is not authorized, just throw the exception.
  def check_authorization
    raise User::NotAuthorized unless current_user.admin?
  end
end

Force SSL

production.rb config.force_ssl

per controller: force_ssl or force_ssl :only|:except => :cheeseburger

http://guides.rubyonrails.org/routing.html

Routing

GET /patients/17

match "/patients/:id" => "patients#show"

@patient = Patient.find(17)

<%= link_to "Patient Record", patient_path(@patient) %>

Resources

DELETE /photos/17

resources :photos

index, show, new, edit, create, update and destroy

HTTP Verb Path action used for
GET /photos index display a list of all photos
GET /photos/new new return an HTML form for creating a new photo
POST /photos create create a new photo
GET /photos/:id show display a specific photo
GET /photos/:id/edit edit return an HTML form for editing a photo
PUT /photos/:id update update a specific photo
DELETE /photos/:id destroy delete a specific photo

first match wins

Paths and URL's

  • photos_path - /photos
  • new_photo_path - /photos/new
  • edit_photo_path(:id) - /photos/:id/edit (for instance, edit_photo_path(10) returns /photos/10/edit)
  • photo_path(:id) - /photos/:id (for instance, photo_path(10) returns /photos/10)

use _url instead of _path to get full url, host, port and path.

Multiple resources defined at once

resources :photos, :books, :videos

Namespaces

namespace :admin do
  resources :posts, :comments
end
# /admin/posts admin_posts_path
# /admin/posts/new new_admin_posts_path

Namespaced Controllers without namespacing the routes

to route /posts (without the prefix /admin) to Admin::PostsController

scope :module => "admin" do
  resources :posts, :comments
end
# resources :posts, :module => "admin" (one liner version if its just for a single resource)

aaand to namespace the route but not the Controller you can:

scope "/admin" do
  resources :posts, :comments
end

Nested Resources

When resources are logically children of others

Magazine has_many ads, Ad belongs_to Magazine

resources :magazines do
  resources :ads
end

Magazine routes are created as normal.

The set of Ad routes are also created but are all prefixed with /magazines/magazine_id

Helpers are different too, magazine_ads_url, edit_magazine_ad_path, first parameter to these is always an instance of Magazine

Rule of thumb never nest more than 1 level deep

Creating urls from objects

as opposed to passing in the id's of the resources

<%= link_to "Ad details", magazine_ad_path(@magazine, @ad) %>

<%= link_to "Ad details", url_for([@magazine, @ad]) %>

<%= link_to "Ad details", [@magazine, @ad] %>

<%= link_to "Magazine details", @magazine %>

Additional routes for a resource

resources :photos do
  member do
    get 'preview'
    post 'publish'
  end
end

# alternate style
resources :photos do
  get 'preview', :on => :member
end

Additional Collection routes

resources :photos do
  collection do
    get 'search'
  end
end

# alternate style
resources :photos do
  get 'search', :on => :collection
end

if you're adding lots of routes do you actually have another resource hiding in there?

Non-Resourceful Routes

classic default route:

match ':controller(/:action(/:id))' parentheses denote an optional parameter

match ':controller/:action/:id/with_user/:user_id'

Querystring still functions as normal: /photos/show/1?user_id=2 gives params {:id => "1", :user_id => "2"}

match 'photos/:id' => 'photos#show'

match 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }

Naming routes

match 'exit' => 'sessions#destroy', :as => :logout, :via => :get gives us a helper logout_path and logout_url

Constraints

match 'photos/:id' => 'photos#show', :constraints => { :id => /[A-Z]\d{5}/ }

You can deal with subdomains using constraints too.

Advanced Constraints: use a class that responds to matches?

Redirecting

routing can also do powerful redirects

Root

root :to => 'pages#main' # this should go at the top of the routing file

Customizing Resourceful Routes

resources :photos, :only => [:index, :show]

resources :photos, :except => :destroy

resources :photos, :controller => 'images' paths and helpers use photos but the controller will be Images

resources :photos, :constraints => {:id => /[A-Z][A-Z][0-9]+/} /photos/RR27

resources :photos, :as => "images" images will now be in the helpers, e.g. images_path

resources :photos, :path_names => { :new => 'make', :edit => 'change' } photos/make and photos/1/change but actual action names are unchanged

namespacing

scope "admin" do
  resources :photos, :as => "admin_photos"
end
 
resources :photos

Override singular

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular 'tooth', 'teeth'
end

Inspecting and Testing

rake routes

Testing routes

assert_generates "/photos/1", { :controller => "photos", :action => "show", :id => "1" }
assert_generates "/about", :controller => "pages", :action => "about"
assert_recognizes({ :controller => "photos", :action => "show", :id => "1" }, "/photos/1")
assert_recognizes({ :controller => "photos", :action => "create" }, { :path => "photos", :method => :post })

even better

assert_routing({ :path => "photos", :method => :post }, { :controller => "photos", :action => "create" })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment