Skip to content

Instantly share code, notes, and snippets.

@JuanitoFatas
Forked from nightire/Changes in Rails 4_1.md
Last active December 22, 2015 08:39
Show Gist options
  • Save JuanitoFatas/6446423 to your computer and use it in GitHub Desktop.
Save JuanitoFatas/6446423 to your computer and use it in GitHub Desktop.

Routes

小心地使用 Match(Rails 3 已實現)

Rails 3 提供了match 方法供我們自定義routes,然而我們要小心使用它以避免“跨站腳本攻擊”(XSS Attack)。比如像這樣的 routes:

注:(r3 代表Rails 3,r4 代表Rails 4)

# routes.rb
match '/books/:id/purchase', to: 'books@purchase'

用戶可以很輕鬆地使用XSS Attack CSRF Attack,比如使用這樣一個鏈接:

CodeSchool 的Rail 4 教程裡寫的是XSS Attack,經查證和問詢,證明這是CodeSchool 的失誤,應該會很快改正過來,再次先做一個修正,並向受到誤導的朋友致歉

<a href="http://yoursite.com/books/4/purchase">Get It Free!</a>

這會使用GET 去請求這個資源,你絕對不想看到這種情況(你希望的是POST),所以你要限制客戶端可以訪問此資源的方式。例如:

match '/books/:id?purchase', to: 'books@purchase', via: :post # :all 代表匹配所有的HTTP methods

# 或者
post '/books/:id?purchase', to: 'books@purchase'

否則你就會收到如下錯誤提示:

You should not use the match method in your router without specifying an HTTP method. (RuntimeError)


新的 HTTP Verb:patch

過去我們使用put 來完成對資源的更新請求,然而put 本身是對整個資源(數據集合)進行更新,若要實現部分資源的更新(單個數據,或是幾個產生變化的數據實體),put 就有點過重了,此時patch 會更加合適。

patch 並不是什麼新東西,此前就一直存在於HTTP 1.1 協議規範之中,只不過這一次Rails 4 把它正式的引入進來。在Rails 4 中,putpatch 都指向controller#update,在更新部分資源時(比如@book)會使用patch,生成類似下例中的頁面元素:

<form action="/books/20" method="post">
  <div style="margin:0;padding:0;display:inline">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="_method" type="hidden" value="patch" /> <!-- 關鍵就是這一行了-->
  </div>
</form>

同時還增加了一個#patch 方法,可以在合適的時候使用:

test "update book with PATCH verb" do
  patch :update, id: @book, book: { title: @book.title }
  assert_redirected_to book_url(@book)
end

Concerns for Routing

Concerns(關注點)是一種組織代碼結構的方式,用來幫助開發者將復雜的邏輯和重複代碼梳理清楚,我們在Rails 4 中多次看到對於Concerns 的設計和實現。先看一段老代碼:

resources :messages do
  resources :comments
  resources :categories
  resources :tags
end

resources :posts do
  resources :comments
  resources :categories
  resources :tags
end

resources :articles do
  resources :comments
  resources :categories
  resources :tags
end

像這樣的代碼存在許多的重複,Rails 4 允許我們重構它:

concern :sociable do
  resources :comments
  resources :categories
  resources :tags
end

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles, concerns: :sociable

可以通過傳遞參數來實現對個例的特化:

concern :sociable do |options|
  resources :comments, options
  resources :categories, options
  resources :tags, options
end

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles do
  concerns :sociable, only: :create
end

甚至我們可以抽​​取出來變成單獨的類:

# app/concerns/sociable.rb
class Sociable
  def self.call(mapper, options)
    mapper.resources :comments, options
    mapper.resources :categories, options
    mapper.resources :tags, options
  end
end

# config/routes.rb
concern :sociable, Sociable

resources :messages, concerns: :sociable
resources :posts, concerns: :sociable
resources :articles do
  concerns :sociable, only: :create
end

拋棄 Ruby 1.8.x

我們都聽說Rails 4 需要Ruby 的版本不能小於1.9.3,不過這一點所引起的變化通常都十分微妙,不容易讓人注意到。

聒噪的 nil

1.8.x 時代,nil.id 是合法的(一切都是對象!),但是不合理,經常惹人厭。於是1.9.2 之後,逐漸使用object_id 來代替,使用舊的id 方法會拋出運行時錯誤:

RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id

Rails 3 無法永遠擺脫這惱人的提示,因為它要同時兼容1.8 和1.9,於是一旦碰上可能會出現的nil.id 就會看到上面那個錯誤

在Rails 4 的世界裡,手起刀落,喀嚓~~~ 從此nil 不再聒噪,世界終於清淨了……

NoMethodError: undefined method `id' for nil:NilClass

線程安全

線程安全的處理在Rails 3 中已有,不過默認是關閉的:

# config/environments/production.rb
MyApp::Application.configure do
  # Enable threaded mode
  # config.threadsafe!
end

這個方法在Rails 4 中不推薦使用,新的線程安​​全機制在默認情況下就已經開啟:

# config/environments/production.rb
MyApp::Application.configure do
  config.cache_classes = true # 阻止類在請求中重新載入,並保證Rack::Lock 不包含在中間件堆棧中
  config.eager_load = true # 在新線程創建前加載全部代碼
end

ActiveRecord

Finders

Book.find(:all, conditions: { author: 'Albert Yu' })

這種方法已經用了很久了吧?在 Rails 4 中,你會看到如下警告:

DEPRECATION WARNING: Calling #find(:all) is deprecated. Please call #all directly instead. You have also used finder options. These are also deprecated. Please build a scope instead of using finder options.

實際上,老式的finders 已經被抽取成了activerecord-deprecated_finders gem,你要還想用就得自己安裝它。

在 Rails 4 中,推薦這樣用:

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

動態的 find_by 也不例外:

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! 太棒了!

統一使用find_by 不僅有更好的一致性,而且更便於接收hash 參數:

book_param = { title: 'Rails 4', author: 'Albert Yu' }
Book.find_by(book_param)

find_by 方法的內部實現其實很簡單:

# activerecord/lib/active_record/relation/finder_methods.rb
def find_by(*args)
  where(*args).take
end

這意味著這樣用也沒有問題:

Book.find_by("published_on < ?", 3.days.ago)

find_or_*

這兩種方法不再推薦使用了:

Book.find_or_initialize_by_title('Rails 4')
Book.find_or_create_by_title('Rails 4')

會拋出如下警告:

DEPRECATION WARNING: This dynamic method is deprecated. Please use eg Post.find_or_initialize_by(name: 'foo') instead.

DEPRECATION WARNING: This dynamic method is deprecated. Please use eg Post.find_or_create_by(name: 'foo') instead.

讓我們從善如流:

Book.find_or_initialize_by(title: 'Rails 4')
Book.find_or_create_by(title: 'Rails 4')

還有一種容易讓人迷惑的用法

Book.where(title: 'Rails 4').first_or_create
# 若找不到…
Book.where(title: 'Rails 4').create

這方法在Rails 3 和Rails 4 裡都可以用,它先是查詢是否有符合條件的記錄,若沒有就以該條件創建一個。聽起來還不錯,然而當存在這樣的代碼時,其表現就不是你想的那樣了:

class Book < ActiveRecord::Base
  after_create :foo
  
  def foo
    books = books.where(author: 'Albert Yu')
    ...
  end
end

產生的 SQL 是:

SELECT "books".* FROM "books" WHERE "books"."title" = 'Rails 4' AND "books"."author" = 'Albert Yu'

注意,這裡的after_create 回調原本是在創建一條記錄後立刻返回__所有作者是Albert Yu 的記錄__,但最終的結果卻是__所有標題是Rails 4 並且作者是Albert Yu 的記錄_ _。這是因為觸發該回調函數的方法調用已經有了title: 'Rails 4' 的作用域,於是產生了作用域疊加。

Rails 4 裡推薦這樣來做:

Book.find_or_create_by(title: 'Rails 4')
# 若找不到…
Book.create(title: 'Rails 4')

這樣就不會產生疊加副作用,真正的SQL 語句如下:

SELECT "books".* FROM "books" WHERE "books"."author" = 'admin'

#update & #update_column

是不是經常被#update_attributes#update_attribute 還有#update_column 搞暈?好消息來了——Rails 4 重新整理了屬性更新的方法,現在的方式簡單明了:

@book.update(post_params) # 會觸發驗證

@book.update_columns(post_params) # 構建SQL 語句,直接執行於數據庫層,不會觸發驗證

就這倆,不會搞錯了吧?以前的方式也還能用,但是不排除會被廢棄。既然Rails 4 提供了更好用的方法,那就不要再猶豫了。

Model.all

也不是所有的變化都那麼顯而易見的令人愉悅,一部分人大概會對接下來的變化感到不適應。以前普遍認為不要直接使用Model.all,因為這會產生很嚴重的性能問題,開發者更傾向於先對Model 進行scope:

def index
  @books = Book.scoped
  if params[:recent]
    @books = @books.recent
  end
end

然而,Rails 4 會拋出如下警告:

DEPRECATION WARNING: Model.scoped is deprecated. Please use Model.all instead.

WTF? Model.all 又回來了?

沒錯。不過你不用擔心,Rails 4 裡的Model.all 不會立即執行對數據庫的查詢,而僅僅是返回一個ActiveRecord::Relation,你可以繼續進行鍊式調用:

def index
  @books = Book.all # 我不會碰數據庫的哦,直到你告訴我下一個條件…
  if params[:recent]
    @books = @books.recent # 這時候我才會行動
  end
end

當然,這並不是說不能用scoped model 了,只不過是多了一層防範措施,以減少初學者不小心造成的性能問題。

ActiveRecord

Scopes

順著上一節的話題,我們繼續講講Scopes。在Rails 4 當中,eager-evaluated scopes 不再推薦使用了,因為通常沒搞清對象(obejct)的熱心(eager)幫助往往會幫倒忙!

附註:eager 這個詞在這裡不好翻譯,其原意是有“熱情”、“渴望”的含義,在這裡代表“在預先不知道查詢請求的對象時就先做查詢”。這一點固然有積極的意義,但有時候也會帶​​來意料不到的結果。請看下文:

舉個例子:

scope :sold, where(state: 'sold')
default_scope where(state: 'available')

這樣定義scope 就稱之為eager-evaluated,因為在進行查詢之前並不知道具體要調用該查詢的對像是誰。在 Rails 4 中,以上代碼會拋出警告:

DEPRECATION WARNING: Using #scope without passing a callable object is deprecated

DEPRECATION WARNING: Calling #default_scope without a block is deprecated

按照提示所說,你需要在定義scope 的時候傳遞一個proc 對象,所以修正的方法也很簡單:

scope :sold, -> { where(state: 'sold') }
default_scope -> { where(state: 'available') }

為什麼呢?看一個實際的例子就明白了:

scope :recent, where(published_at: 2.weeks.ago)

這段代碼的問題就在於2.weeks.ago 的求值只會在這個class 載入時發生一次,以後再調用的時候你還是會得到一模一樣的值。

scope :recent, -> { where(published_at: 2.weeks.ago) }
scope :recent_red, recent.where(color: 'red')

轉變成proc 對像後,當你再次調用它就會重新求值(上例第二行,recent_red 調用recent,recent 會重新求值),於是此問題就解決了。

當然,你應該在所有的scopes 裡應用這一原則,因此上例最終應寫成:

scope :recent, -> { where(published_at: 2.weeks.ago) }
scope :recent_red, -> { recent.where(color: 'red') }

這個變化可能不是那麼新鮮,畢竟多數開發者在Rails 3 的時候就是這麼處理的,Rails 4 只是​​對未處理過的scopes 報出警告而已,算是一個小小的變化。


Relation#not

#not 是一個新方法,而且非常好用。我們先來看一段代碼:

Book.where('author != ?', author)

你知道這個查詢有什麼問題麼?大部分情況下它工作良好,但如果author = nil 的話,Rails 會產生如下SQL 語句:

SELECT "posts".* FROM "posts" WHERE (author != NULL)

它能用,但是最後括號裡的部分不符合SQL 的語法規則,這會讓許多“代碼潔癖”患者感到寢食難安的! (玩笑)所以他們通常會寫出如下無可奈何的臨時解決方案:

if author
  Book.where('author != ?', author)
else
  Book.where('author IS NOT NULL')
end

現在,同樣的需求在Rails 4 裡可以這樣寫:

Book.where.not(author: author)

該查詢生成的 SQL 語句非常標準:

SELECT "posts".* FROM "posts" WHERE (author IS NOT NULL)

Relation#none

#none 也是和#not 一樣棒的新方法,考察一下這段代碼:

Class User < ActiveRecord::Base
  def visible_posts # 查詢可見的帖子...
    case role # ...基於用戶的角色
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      ???
    end
  end
end

那麼,對於Bad User 我們要求不返回任何帖子,你要怎麼做?比較直覺性的做法就是返回一個空數組[],但是對於下面的代碼來說:

@posts = current_user.visible_posts
@posts.recent

會報錯:

NoMethodError: undefined method `recent' for []:Array

本著“頭疼醫頭,腳疼醫腳”的精神……你可以這麼搞:

@posts = current_user.visible_posts

if @posts.any?
  @posts.recent
else
  []
end

但是這太醜了,不是麼?你必須要檢查查詢數組裡有沒有東西,然後在明知沒有的情況下再返回代表“沒有”的空數組……多愚蠢啊~為什麼Rails 不能幫我們檢查是否“沒有”呢?在 Rails 4 裡這變成了可能:

Class User < ActiveRecord::Base
  def visible_posts
    case role
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      Post.none # 空即是空,無便是無……
    end
  end
end

上例中的Post.none 並不只是返回空數組,而是返回一個不去碰數據庫的ActiveRecord::Relation,你可以獲得如下的查詢:

@posts = current_user.visible_posts
@posts.recent # 根據前文的條件,這個方法會產生三個可能的查詢:

# 1
Post.where(country: country).recent

# 2
Post.published.recent

# 3
Post.none.recent # 不會報錯,這是對的查詢

Relation#order

#order 方法現在產生了一些新的變化,主要是針對生成的SQL 語句,以下簡明列舉:

class User < ActiveRecord::Base
  default_scope -> { order(:name) }
end

User.order("created_at DESC")

以上代碼在3 和4 裡產生了有所區別的SQL:

/*in r3*/
SELECT * FROM users ORDER BY name asc, created_at desc

/*in r4*/
SELECT * FROM users ORDER BY created_at desc, name asc

另外,現在可以用symbol 來代表排序的查詢條件了:

# in r3
User.order('created_at DESC')
User.order(:name, 'created_at DESC')

# in r4
User.order(created_at: :desc)
User.order(:name, created_at: :desc)

這麼做的好處還是為了增強一致性,並且利於使用hash 傳入查詢條件。


Relation#references

說到字符串形式的查詢條件,在Rails 4 中對於這樣的代碼:

Post.includes(:comments).where("comments.name = 'foo'")

會拋出警告:

DEPRECATION WARNING: It looks like you are eager loading table(s) (one of: posts, comments) that are referenced in a string SQL snippet. (...)

所以你必須對字符串形式的查詢顯式聲明其引用的表是哪一個,就像這樣:

Post.includes(:comments).where("comments.name = 'foo'").references(:comments)

然而對於hash 形式的條件傳遞,就不需要特意聲明了:

Post.includes(:comments).where(comments: { name: 'foo' })
# or
Post.includes(:comments).where('comments.name' => 'foo' })

像下面這樣沒有條件的查詢,儘管是字符串也無需聲明references

Post.includes(:comments).order('comments.name')

Relation#pluck

pluck 方法現在可以接受多個參數了(每個參數代表數據庫表中的一個字段):

Person.pluck(:id, :name)

現在將會返回包含兩個字段的記錄了,一個小小的但是很有用的改進。


Relation#unscope

Post.comments.except(:order)

像上面這一句代碼,你以為會排除order 的排序,但卻不盡然。因為如果Comment 的default_scope 是帶有order 的話,except 並無法改變Post.comments 的查詢結果。幸好 Rails 4 中多了一個新方法:

Post.comments.unscope(:order) == Post.comments.order

這樣會確保你想要的結果,而不必擔心default_scope 所造成的影響。另外,unscope 方法是支持多個參數的。


Partial inserts

當向數據庫插入新的記錄的時候,Rails 會對比缺省值,然後只把發生變化的字段放進INSERT 語句裡,剩下的部分由數據庫自動填充。這一變化會使得增加記錄效率更高,移除數據庫字段也會更加安全。

ActiveModel

ActiveModel::Model

Rails 3 中增加了ActiveModel 使得我們可以創建和ActiveRecord 一樣的模型,擁有幾乎全部功能卻不需要和數據庫關聯,就像這樣:

class SupportTicket
  include ActiveModel::Conversion
  include ActiveModel::Validations
  extend ActiveModel::Naming

  attr_accessor ​​:title, :description

  validates_presence_of :title
  validates_presence_of :description
end

於是,你可以為其生成關係表單,做條件驗證等等,非常方便。在Rails 4 中,對ActiveModel 做了小小的改進,現在你可以直接include 它的“精簡版”:

class SupportTicket
  include ActiveModel::Model

  attr_accessor ​​:title, :description

  validates_presence_of :title
  validates_presence_of :description
end

ActiveModel::Model 是一個“混編模組”:

# activemodel/lib/active_model/model.rb
def self.included(base)
  base.class_eval do
    extend ActiveModel::Naming
    extend ActiveModel::Translation
    include ActiveModel::Validations
    include ActiveModel::Conversion
  end
end

Easy and clear!

Association in Rails 4

相比Rails 3,Rails 4 裡的Association 返回的不再是數組而是一個集合代理(CollectionProxy),這一變化是好是壞應該說莫衷一是,具體產生的影響由於演示起來篇幅過長,所以請移步這篇博客

總結起來就是輸出到客戶端的關係數據會有所變化,會影響到JSON API,不過在適應了規則之後,前端工程師處理這些小變化應該是沒什麼問題的。

Others

Migration Helper

#create_join_table

Migration 文件里新添加了一個Helper method, 專門用於為HABTM 關係創建關聯表:

create_join_table :categories, :products, :id => false do |f|
  f.integer :categories_id, :null => false
  f.integer :products_id, :null => false
end

現在主鍵會自己初始化為nil,除非你用別的值覆蓋它。

self.disable_ddl_transaction!

如果你選用的數據庫支持DDL Transaction,那麼所有的數據庫遷移會被包裹在一個事務中完成;然而某些SQL 命令無法在事物內部成功執行,這會造成遷移的失敗。在Rails 4 中,你可以把這些造成失敗的命令抽取出來放在一個單獨的migration 裡,然後使用這個方法來禁止事務處理:

class ChangeSth < ActiveRecord::Migration
  self.disable_ddl_transaction!
  def change
    # some SQLs those can not execute in a transaction
  end
end

Schema Cache Dump

在產品環境中,Rails 應用在初始化的時候會把所有model 的數據庫模式(schema)載入至一個schema cache(模式緩存)中。對那些擁有龐大數量的models 的應用程序而言,Rails 4 提供了schema cache dump(模式緩存轉儲)的新功能,用來加速應用程序的啟動。你可以使用這個 rake task:

$ RAILS_ENV=production bundle exec rake db:schema:cache:dump

這會生成一個db/schema_cache.dump 文件,Rails 用它來加載SchemaCache 實例的內部狀態。

你可以選擇關閉這個功能,編輯config/production.rb 文件,添加這一行:

config.active_record.use_schema_cache_dump = false

如果你要清除 schema cache,執行:

$ RAILS_ENV=production bundle exec rake db:schema:cache:clear
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment