Ga Tech

1.01 everyday

Rails 4(3)

紀錄 Rails 3 to Rails 4 的一些改變。

SCOPES

在 Rails 3 當中,scope 的寫法如下(稱為 eager-evaluated scoped):
scope :sold, where(state: ‘sold’)
default_scope where(state: ‘available’)

這樣的寫法在 Rails 4 也被摒棄掉,而改為:
scope :sold, ->{ where(state: ‘sold’) }
後面必須改為 proc object 才行。

default_scope 後面則可以接 proc object 或 block:
default_scope { where(state ‘available’) }
default_scope ->{ where(state ‘available’) }

EAGER-LOADED SCOPES

Rails 3 scope 寫法被摒棄的原因如下:
scope :recent, where(published_at: 2.weeks.ago)

在 class 被載入時,date 只會解析一次,之後用到這個 scope 時就會沿用之前所解析的 date,因此 published_at 就固定在那天而不會改變了。

另一個例子:
scope :recent, ->{ where(published_at: 2.weeks.ago) }
scope :recent_red, recent.where(color: ‘red’)

雖然第一個 scope 有正確使用 proc object 來避免 date 固定,但第二個 scope 卻仍然會使用先前被解析的 recent,而導致 date 不正確。

為了避免這些情形,Rails 4 才全面改用 proc object 確保這些 condition 都會在需要的時候進行解析:
scope :recent, ->{ where(published_at: 2.weeks.ago) }
scope :recent_red, ->{ recent.where(color: ‘red’) }

RELATION#NONE

在 Rails 3 當中,我們常常會列出某個 user 的 posts,再針對這些 posts 進行處理。當 posts 為 [] 時,就必須特別處理(否則會出現 NoMethodError),比如:

models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
class User < ActiveRecord::Base
def visible_posts
case role
when 'Country Manager'
Post.where(country: country)
when 'Reviewer'
Post.published
when 'Bad User'
[] # represents empty collectoin
end
end
controllers/posts_controller.rb
1
2
3
4
5
6
7
8
...
@posts = current_user.visible_posts
if @posts.any?
@posts.recent
else
[]
end

看起來非常的繁瑣。

Rails 4 提供了 none method 來解決:

models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
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 # returns ActiveRecord::Relation
end
end
controllers/posts_controller.rb
1
2
3
...
@posts = current_user.visible_posts
@posts.recent

@posts.recent 有可能會回傳下列三種:
Post.where(country: country).recent
Post.published.recent
Post.none.recent

Post.none 不會使用到 database,並且會回傳 ActiveRecord::Relation,且後續再 chain 其他 method 比如 recent 的時候,都會是 empty collection。

RELATION#NOT

在 Rails 3 當中,有時候會想要撈出 “非”某個 user 的 posts,通常會這樣寫:
Post.where(‘author != ?’, author)

但是當 author 為 nil 的時候,就會產生出錯誤的 query :
SELECT “posts”.* FROM “posts” WHERE (author != NULL) # incorrect SQL syntax

所以往往會多加一些條件式來避免產生錯誤的 query:

1
2
3
4
5
if author
Post.where('author != ?', author)
else
Post.where('author IS NOT NULL')
end

太麻煩了…

Rails 4 提供新的寫法,只要一行就可以解決:
Post.where.not(author: author)

會自動產生:
SELECT “posts”.* FROM “posts” WHERE (author IS NOT NULL)

## RELATION#ORDER

假設我們有個 User model,裡頭有一個 default_scope:
models/user.rb
1
2
3
class User < ActiveRecord::Base
default_scope { order(:name) }
end

然後在某個地方又使用了一次 order
User.order(“created_at DESC”)

猜猜看哪個 order 會先被執行?

在 Rails 3 當中,會先針對 name 進行排序:
SELECT * FROM users ORDER BY name asc, created_at DESC

順序是越後面(越新)使用的 order 會往後加。

但是在 Rails 4 則剛好相反,後來使用的 order 其順序反而會越前面:
SELECT * FROM users ORDER BY created_at DESC, name asc

more about RELATION#ORDER

在 Rails 3,反向排序的寫法是:
User.order(‘created_at DESC’)

但是在 Rails 4 當中,DESC 可以改用 hash:
User.order(created_at: :DESC)

如果要多個 order 的話,可以這樣寫:
User.order(:name, created_at: :desc)

這樣會產生:
SELECT * FROM users ORDER BY name asc, created_at DESC
(order 預設為 asc)

RELATION#REFERENCES

在 Rails 3 當中假設一個情境:
Post.includes(:comments).where(“comments.name = ‘foo’”)
Post 針對 comments 使用 eager loading,並想要撈出 comments.name 為 foo 的 string。
這樣的寫法在 Rails 4 已經被摒棄掉了,原因在於 Rails 必須深入到 SQL string 當中來確認參照了什麼 table,這種寫法被認為難以理解且很容易出錯。
Rails 4 裡,必須要明確地寫出是參照哪個 table:
Post.includes(:comments).where(“comments.name = ‘foo’”).references(:comments)

但在某些情況下是不用加 references 的,比如 condition 是以 hash 的形式來下:
Post.includes(:comments).where(comments: { name: ‘foo’} )
Post.includes(:comments).where(‘comments.name’ => ‘foo’)

或是純粹排序也不用:
Post.includes(:comments).order(‘comments.name’)

ACTIVEMODEL::MODEL

在 Rails 3 裡,我們可以使用 active model 把任何 Ruby class 所產生的 objects 變成 active record object,且不需要任何對應的 database table。比如:

1
2
3
4
5
6
7
8
9
10
class SupportTicket
include ActiveModel::Conversion
include ActiveModel::Validations
extend ActiveModel::Naming
attr_accessor :title, :description
validates_presence_of :title
validates_presence_of :description
end

透過 mixed-in ActiveModel 的 module,就可以變成 active record object 出現在 form_helper
form_for(@support_ticket) do |f|

end

或是類似 ActiveRecord model:
SupportTicket.new(support_params)
@support_ticket.valud?
@support_ticket.errors
@support_ticket.to_param

在 Rails 4 裡,mixed-in 的寫法可以簡化成一段即可:

1
2
3
4
5
6
7
8
class SupportTicket
include ActiveModel::Model
attr_accessor :title, :description
validates_presence_of :title
validates_presence_of :description
end

Model 這個 module 就包含了 Rails 3 寫法的那些東東:

activemodel/lib/active_model/model.rb
1
2
3
4
5
6
7
def self.includes(base) base.class_eval do
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
end
end

source: CodeSchool

Comments