The English version of this post is available here

Rails 6.1 預計今年會推出,其中包含了 ActiveModel Errors 的大改變。我想來解釋一下改變的原因,以及開發者該怎樣做準備。

每個錯誤都被包成一個物件

當 model attribute 有錯誤資料時,呼叫 valid? 會在 errors.messages 這個 hash 中產生每個 attribute 相對應的錯誤訊息。不過在 Rails 5.0 開始,又新增了另一份叫做 details 的 hash,讓開發者還能取得更詳細的錯誤資訊。

這兩個hash應該要同步一對一,但是要把messages中某訊息對應到details是一件很瑣碎的工作。我們必須記得array index後才能在另一邊取得資料。這兩個 hash 也會在某些特定狀況變得不一致。有學過物件導向的大家應該都能了解資料不封裝的話,資料一致性就容易被破壞。所以在 Rails 6.1 ,我們把單一錯誤的所有資訊都封裝在一個物件中。

現在book.errors其實是一陣列的Error物件.

大改變通常就會伴隨著deprecation或是需要開發者修復的breaking change,以下我會把最重要的改變列出來:

如何更新?

messagedetails

原本 messages, full_messages and details 這些方法都是一次提供所有錯誤資訊。現在新的 Error 物件只提供關於自己的資料:

e = ActiveModel::Error.new(book, :title, :too_long, limit: 1000)
e.message # 'is too long (maximum is 1000 characters)'
e.full_message # 'Title is too long (maximum is 1000 characters)'
e.detail # { limit: 1000, error: :too_long }

Enumeration 方法

原本 errors 運作類似 hash,比如說我們可以:

book.errors.each do |attribute, error_message|
  puts attribute
  puts error_message
end

為了相容性,在 Rails 6.1 中,上面的程式會出現 deprecation 警告。不過,要是 block 參數不是兩個而是只有一個時,列舉(enumeration)的方法就會 yield 出 Error 物件:

book.errors.each do |error|
  puts error.attribute
  puts error.message
end

有些列舉方法是沒有 block 的,如 first ,這些方法以前會回傳只有一個陣列,現在則是會回傳 Error 物件。這是breaking change,要是你的網頁有用到的話,麻煩更改一下。

避免直接更改 hash

以往,你可以直接插入新的錯誤訊息到 hash 中:

book.errors[:title] << 'is not interesting enough.'
book.errors[:title].clear

現在這些操作會給你 deprecation 警告,因為在未來,這些 hash 是每次呼叫時動態產生的,而不再是資料存放的源頭。要操作的話,請使用 add 方法或其他 Array 方法:

book.errors.add(:title, 'is not interesting enough.')
book.errors.delete_all { |error| error.type == :too_long }

以下的資料更改未來會沒有作用:

  • errors 本身類似 hash 的操作 (e.g. errors[:foo] = 'bar')
  • errors#messages 回傳的 hash (e.g. errors.messages[:foo] = 'bar')
  • errors#details 回傳的 hash (e.g. errors.details[:foo].clear)

消除類似hash的介面

因為我們要把errors變成陣列,所以類hash的介面之後也會被移除,像是:

  • errors#slice!
  • errors#values
  • errors#keys

其他

  • errors#to_xml 會被移除
  • errors#to_h 會被移除,但是可以用errors#to_hash替代。

新東西

新的where方法能用來尋找特定的錯誤。要注意的是它跟你熟悉的 ActiveRecord#where 有一點不同,我們可以依照以下的參數做篩選:

  • model attribute (Symbol, 必須)
  • error type (Symbol, 非必須)
  • options (hash, 非必須)

篩選是依照有提供的參數:

book.errors.where(:name) # => 所有跟 name 相關的錯誤
book.errors.where(:name, :too_short) # => 所有 name 錯誤中是 too_short 類的錯誤
book.errors.where(:name, :too_short, minimum: 2) # => 所有 name 錯誤中是 too_short 類的錯誤,而且最低要求是 2

新的import 方法是用來匯入並包裝其他地方的錯誤。這對association或是巢狀service object很有幫助,未來應該能開啟新的應用方向。原本的merge!已經使用import了。

delete 方法以往只能刪除所有跟某 attribute 相關的錯誤。以後它也能做出更細緻的篩選,比如說只刪除 name 錯誤中 too_short 錯誤。

結論

若你有興趣,可以閱讀原始的 pull request。你也可以閱讀官方API文件

Rails 6.1 還沒正式發表,這篇又是照我一年前的記憶寫的,所以有些錯誤。在此先說聲抱歉。

寫這篇是希望大家在 6.1 塵埃落定之前提出建議,以便作出修正或是減少升版號的痛苦。所以歡迎大家提供自己的想法。感謝~