Rails Cheet Sheet

本文旨在记录常用的rails相关的命令

Concepts

Model Layer: 这一层由业务模型类组成。在Rails中有两种类型。1. 是继承自ActiveRecord::Base。2. 实现了ActiveModel中相应接口的普通ruby类。

Controller Layer: 这一层主要用来处理http请求。在Rails中由ActionDispatchActionController组成。

View Layer: ActionView通过模板生成各种资源。

Basic Startup

初始化项目以及基本使用。

# Initialize a proejct
rails new demo
cd demo
rake about          # try to see if everything is OK
rails server        # start web server

Rails Generate

代码生成

rails generate controller Say hello goodbye   
rails g scaffold Product title:string  # generate module/action/view for Product, upper case seems not of usage
rails g migration add_quantity_to_line_items quantity:integer  # name of the migration will help rails to guess what's your meanning
rails g model payment_type name:string # generate model
rails destroy  model Oops     # opposite of generate

Debug

rails console
rails dbconsole
rails runner script/load_orders.rb
prd = Product.new
prd.save # => false
prd.errors.full_messages # => view error message

Database Console

mysqld --verbose --help # lookup mysql configuration
sqlite3 -line db/development.md

Rake - Ruby Make

rake -T doc # view document related task
rake --describe task # get detail description of a task
rake --tasks         # list available task for this project
rake db:create       # create the database by database.yml
rake db:migrate      # make schema change
rake db:rollback     # revert schema change
rake db:seed         # load data defined in db/seeds.rb
rake db:migrate:status # track status of migrate
rake assets:clean      # remove compiled assets
rake assets:precompile # compile all assets
rake doc:app           # generates documentation

Action Model

Attribute Magic

class Person
  include ActiveModel::AttributeMethods

  attribute_method_prefix 'clear_'
  define_attribute_methods :name, :age

  attr_accessor :name, :age

  def clear_attribute(attr)
    send("#{attr}=", nil)
  end
end

person = Person.new
person.clear_name
person.clear_age

Callbacks

class Person
  extend ActiveModel::Callbacks
  define_model_callbacks :create

  def create
    run_callbacks :create do
      # Your create action methods here
    end
  end
end

Tracking value changes

class Person
  include ActiveModel::Dirty

  define_attribute_methods :name

  def name
    @name
  end

  def name=(val)
    name_will_change! unless val == @name
    @name = val
  end

  def save
    # do persistence work
    changes_applied
  end
end

person = Person.new
person.name             # => nil
person.changed?         # => false
person.name = 'bob'
person.changed?         # => true
person.changed          # => ['name']
person.changes          # => { 'name' => [nil, 'bob'] }
person.save
person.name = 'robert'
person.save
person.previous_changes # => {'name' => ['bob, 'robert']}

Adding Errors Inteface to Objects

class Person

  def initialize
    @errors = ActiveModel::Errors.new(self)
  end

  attr_accessor :name
  attr_reader   :errors

  def validate!
    errors.add(:name, "cannot be nil") if name.nil?
  end

  def self.human_attribute_name(attr, options = {})
    "Name"
  end
end

person = Person.new
person.name = nil
person.validate!
person.errors.full_messages
# => ["Name cannot be nil"]

Model name introspection

class NamedPerson
  extend ActiveModel::Naming
end

NamedPerson.model_name        # => "NamedPerson"
NamedPerson.model_name.human  # => "Named person"

making objects serializable

class SerialPerson
  include ActiveModel::Serialization

  attr_accessor :name

  def attributes
    {'name' => name}
  end
end

s = SerialPerson.new
s.serializable_hash   # => {"name"=>nil}

class SerialPerson
  include ActiveModel::Serializers::JSON
end

s = SerialPerson.new
s.to_json             # => "{\"name\":null}"

class SerialPerson
  include ActiveModel::Serializers::Xml
end

s = SerialPerson.new
s.to_xml              # => 

I18n support

class Person
  extend ActiveModel::Translation
end

Person.human_attribute_name('my_attribute')
# => "My attribute"

Validation

class Person
  include ActiveModel::Validations

  attr_accessor :first_name, :last_name

  validates_each :first_name, :last_name do |record, attr, value|
    record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
  end
end

person = Person.new
person.first_name = 'zoolander'
person.valid?  # => false

Custom Validators

class HasNameValidator < ActiveModel::Validator
  def validate(record)
    record.errors[:name] = "must exist" if record.name.blank?
  end
end

class ValidatorPerson
  include ActiveModel::Validations
  validates_with HasNameValidator
  attr_accessor :name
end

p = ValidatorPerson.new
p.valid?                  # =>  false
p.errors.full_messages    # => ["Name must exist"]
p.name = "Bob"
p.valid?                  # =>  true

ActionRecord

Validation

class Product < ActiveRecord::Base
  validates :title, presence: true
  validates :terms, acceptance: true
  validates :password, confirmation: true
  validates :username, exclusion: { in: %w(admin superuser) }
  validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create }
  validates :age, inclusion: { in: 0..9 }
  validates :first_name, length: { maximum: 30, message: 'must be less than 30' }
  validates :age, numericality: true
  validates :username, presence: true
  validates :username, uniqueness: true
end

Associations

class Firm < ActiveRecord::Base
  has_many   :clients
  has_one    :account
  belongs_to :conglomerate
end

How to add connection table in rails

Table LineItem is connection table for product and cart.

rails generate scaffold LineItem product:references cart:belongs_to
class LineItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :cart
end

To add method for reverse finding from Cart we should modify Cart's model ruby class Cart < ActiveRecord::Base has_many :line_items, dependent: :destroy end

Aggregations of value objects

值对象的关联。

class Account < ActiveRecord::Base
  composed_of :balance, class_name: 'Money',
              mapping: %w(balance amount)
  composed_of :address,
              mapping: [%w(address_street street), %w(address_city city)]
end

Transactions

# Database transaction
Account.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

Create Records

an_order = Order.new
an_order.name = "Dave Thomas"
an_order.save

Order.new do |do|
  o.name = "Dave"
  o.save
end

anOrder = Order.new(
  name: "Mike")
anOrder.save

anOrder = Order.create(name: "Jason", email: "x@qad.com")

Attribute Query Method

user = User.new(name: "David")
user.name? # => true

anonymous = User.new(name: "")
anonymous.name? # => false

Find Records/Where Clauses

使用Where Clause,find_by等函数,保持查找函数的统一性。

Book.where(author: 'Albert Yu')

Book.find_all_by_title('Rails 4') # r3 way
Book.find_last_by_author('Albert Yu') # r3 way

Book.where(title: 'Rails 4') # r4 way
Book.where(author: 'Albert Yu').last # r4 way

Book.find_by_title('Rails 4') # 接收单个参数的用法在 r3 & r4 都可以
Book.find_by(title: 'Rails4') # 不过 r4 更偏爱这样写

Book.find_by_title('Rails 4', conditions: { author: 'Albert Yu' }) # 这就不好了,得改
Book.find_by(title: 'Rails4', author: 'Albert Yu') # Wow! 太棒了!

# 容易使人迷惑的用法
Book.where(title: 'Rails 4').first_or_create
# 若找不到…
Book.where(title: 'Rails 4').create

Where Clause的详细用法

name = params[:name]
pos = Order.where(["name = ? and pay_type = 'po'", name])

name = params[:name]
pay_type = params[:pay_type]
pos = Order.where("name = :name and pay_type = :pay_type",
pay_type: pay_type, name: name)

pos = Order.where("name = :name and pay_type = :pay_type",
params[:order])

pos = Order.where(params[:order])

pos = Order.where(name: params[:name],
pay_type: params[:pay_type])

# Doesn't work
User.where("name like '?%'", params[:name])

# Works
User.where("name like ?", params[:name]+"%")

Subsetting the Records found

# The view wants to display orders grouped into pages,
# where each page shows page_size orders at a time.
# This method returns the orders on page page_num (starting
# at zero).
def Order.find_on_page(page_num, page_size)
  order(:id).limit(page_size).offset(page_num*page_size)
end

orders = Order.where(name: 'Dave').
order("pay_type, shipped_at DESC").
limit(10)

Lock of Active Record

Account.transaction do
  ac = Account.where(id: id).lock("LOCK IN SHARE MODE").first
  ac.balance -= amount if ac.balance > amount
  ac.save
end

Statics of Result

average = Order.average(:amount) # average amount of orders
max = Order.maximum(:amount)
min = Order.minimum(:amount)
total = Order.sum(:amount)
number = Order.count

Scopes : to reuse simple data.

class Order < ActiveRecord::Base
  scope :last_n_days, lambda { |days| where('updated < ?' , days) }
  scope :checks, -> { where(pay_type: :check) }
end

orders = Order.last_n_days(7)
orders = Order.checks.last_n_days(7)

Update on ActiveRecord

# fetch and update record, id is used as first parameter
order = Order.update(12, name: "Barney", email: "barney@bedrock.com")
# where clause is the second parameter
# return result depends on database adapter
result = Product.update_all("price = 1.1*price", "title like '%Java%'")

Delete Rows

# delete method bypass the validation and ActiveRecord callback
Order.delete(123)
User.delete([2,3,4,5])

# when no condition is given, all rows are deleted.
Product.delete_all(["price > ?", @expensive_price])

Active Record Callback

class Order < ActiveRecord::Base
  before_validation :normalize_credit_card_number
  after_create do |order|
     logger.info "Order #{order.id} created"
  end
  protected
  def normalize_credit_card_number

   # Notice we have to use self. since this is class method???/
    self.cc_number.gsub!(/[-\s]/, '')
  end
end

Serialized Object to Database

class User < ActiveRecord::Base
serialize :preferences
end

user = User.create(preferences: { "background" => "black", "display" => large })
User.find(user.id).preferences # => { "background" => "black", "display" => large }

Apply Migration

Rails defines a TableDefinition class to describe the schema. It could be init like below: (p.64)

create_table :product do |t|
  t.text :title, limit: 50,
  t.decimal :price, precision:8, scale:2
end

Add Test Data

Make full use of seeds.rb to add test data. Notice ActiveRecord::Base#Create and ActiveRecord::Base#Create! are differenct. The second one will raise an exception if the record is invalid. (p. 69)

Action View

Basic Helper Functions

<%= link_to "Goodbye", say_goodbye_path %>!
<%= link_to "Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %>
<tr class = "<%= cycle('list_line_odd', 'list_line_even') %>" >

Using link_to method in program

Need more knowledge on url routine (p7x)

Modify Unit Test

Not known about products(:one) (p. 81)

Resize image with image_tag

we could pass size as option size: '30x30'

Cache in ruby

Rails 4 supports key based cache which could be referenced here (p.104) We have following steps to enable cache in rails 1. set enable in config 2. change view (*.erb) for fragments that needs to cache 3. set storage method for cache

Dependency for controller code in rails

The following code could run in rails, so what is in the running environment. How could the ActiveSupport, ActiveRecord and Cart be found? We could also pay attention to the private mark. This would prevent rails to treat the set_cart method as controller method.

Answers: 1. modules in app/controllers/concern/ could be accessed in all controllers

module CurrentCart
extend ActiveSupport::Concern
private
  def set_cart
    @cart = Cart.find(session[:cart_id])
    rescue ActiveRecord::RecordNotFound
    @cart = Cart.create
    session[:cart_id] = @cart
  end
end

How to choose Get/Post methods

links_to method will generate Http Get method. button_to will generate POST method.

Parameters of method in ActionPack

There are always kind of function has the following signature (name = nil, options = nil, html_options = nil, &block). I cann't see there is a need for such parameters.

Passing objects between controllers

params is used to pass values between controllers.

Error when using session

cookieOverflow exception throws when add item to cart. By searching the web, it is because session data is too large to store by cookie. Change it to use database to store the value. A new gem activerecord-session_store is introduced.

Q: I still don't know there is the document for 'cookieOverflow'. Not in the api doc. ++ operator is not support in Ruby activerecord#destroy will trigger callback, however activerecord#delete won't.

Flash in rails

Flash is hash-like object, which could pass information to following requests.

Test in Rails

assertequal, assertnot_equal, assert_select usage for controller auto testing

Ruby js

ruby js may generate code with not exists node, in this time, it will be ignored.

Run ruby scripts under rails

Order.transaction do
  (1..100) do |i|
    Order.create (...) 
  end
end
rails runner script/load_orders.rb

Rails send email

rails generate mailer Notifier order_received order_shipped
# create app/mailers/notifier.rb
# create ap/views/notifier/order_received.text.erb
# create ap/views/notifier/order_shipped.text.erb
# create test/functional/notifier_test.rb
Notifer.order_received(@order).deliver

Rails integration test

rails g integration_test user_stories
# create test/integration/user_stories_test.rb

Deploy ruby program on environment

apache used as front-server to process http request, and passenger will dispatch ruby reqest to rails processes.

CRUD With ActiveRecord

Action Pack

Action Dispatch helps to find controller and method for processing request. There are two ways to defined the routes, pre-defined and self-defined.

Depot::Application.routes.draw do
  # pre-defined, 
  # This assumes there is a controller named 'ProductController' and several methods with given names
  resources :products
  resources :comments, except: [:update, :destroy]  # update, destroy is not in need.
  # sef-defined
  resources :products do
    get :who_bought, on: :member  # :collection ~
  end
end

We could use rake routes to review the generated routes. Notes: 'GET /products(.:format) products#index' the /products(.:format) stands for '/product/1.html` or '/product/1.xml'

There are 7 methods that rails generate for us. index,create,new,show,destroy,edit,update

Diff: create and new, create receives a bunch of attributes from POST request and create a new source and save it. new method is only create a empty resource used to gather inputs from client, resource is not saved. It is like initialized method in Base Module

edit and update, edit returns the content of a resource identified by params[:id], update will update the resource by params[:id] with data associated with request.

We could fire a request on client by ruby methods

<% link_to 'Show', product %>
# link_to generate a HTTP GET by default, so need to specify using DELETE instead.
<% link_to 'Destroy', product, method: :delete %>

Rails uses one controller method to handle all types of output(xml/json/html). This may increase difficulty for error handling.

Q&A

  • Where to find the document for phantom method, since they are created dynamically? e.g. find*
  • Rails method has return type, however it is hard to find them.
  • Help function content_tag is no use in my sample
  • Duplicate item is added when click image.
  • How to create class method like before_validate, it could use a class instance as parameter, which method it knows to call.
  • It is not safe to store primary key in the url. The default route of rails needs to changed? p309
  • In RESTful design, the server only works on CRUD of resources. Is there any concern about the validation?? p310
  • Nested routes will provide all resource information on the full path? p318
  • Error handling when user specify illegal URI. http://192.168.33.15:3000/products/1/who_bought.sfas
  • What is a permanent/temporary redirect? what's the differences?
  • How we could build a custom program avoid using ActionPack
  • How to deploy with passenger? There is no content under public directory. p237.
Jason Zhang /
Published under (CC) BY-NC-SA in categories programming  tagged with rails