更多資訊請見 rubyonrails.org:

1 驗證概觀

以下是一個非常簡單的驗證範例

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

如您所見,我們的驗證讓我們知道,沒有 name 屬性的 Person 是無效的。第二個 Person 不會被持久化到資料庫中。

在我們深入探討更多細節之前,我們先來談談驗證如何融入您應用程式的大局中。

1.1 為何要使用驗證?

驗證用於確保只有有效的資料會儲存到您的資料庫中。例如,對於您的應用程式而言,確保每個使用者都提供有效的電子郵件地址和郵寄地址可能很重要。模型層級的驗證是確保只有有效的資料會儲存到您資料庫中的最佳方法。它們與資料庫無關,無法被終端使用者繞過,而且方便測試和維護。Rails 為常見需求提供了內建的輔助方法,並允許您建立自己的驗證方法。

在資料儲存到資料庫之前,還有其他幾種方法可以驗證資料,包括原生資料庫約束、用戶端驗證和控制器層級驗證。以下是優缺點的摘要

  • 資料庫約束和/或預存程序會使驗證機制依賴於資料庫,並可能使測試和維護更加困難。但是,如果您的資料庫被其他應用程式使用,在資料庫層級使用一些約束可能是個好主意。此外,資料庫層級的驗證可以安全地處理某些事情(例如在經常使用的表格中的唯一性),否則這些事情可能難以實現。
  • 用戶端驗證可能很有用,但如果單獨使用,通常不可靠。如果它們是使用 JavaScript 實作的,如果使用者的瀏覽器中關閉了 JavaScript,它們可能會被繞過。但是,如果與其他技術結合使用,用戶端驗證可以為使用者提供在他們使用您的網站時立即回饋的便利方式。
  • 控制器層級的驗證可能很想使用,但往往會變得笨拙,難以測試和維護。在可能的情況下,盡可能保持您的控制器簡單,因為從長遠來看,這將使您的應用程式使用起來很愉快。

在某些特定情況下選擇這些。Rails 團隊認為,模型層級的驗證在大多數情況下是最合適的。

1.2 驗證何時發生?

Active Record 物件有兩種:一種對應於您資料庫內的列,另一種則不對應。當您建立一個新的物件時,例如使用 new 方法,該物件還不屬於資料庫。一旦您對該物件呼叫 save,它將被儲存到適當的資料庫表格中。Active Record 使用 new_record? 實例方法來判斷物件是否已在資料庫中。考慮以下 Active Record 類別

class Person < ApplicationRecord
end

我們可以透過查看一些 bin/rails console 輸出,來了解它是如何運作的

irb> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>

irb> p.new_record?
=> true

irb> p.save
=> true

irb> p.new_record?
=> false

建立並儲存新紀錄將向資料庫發送 SQL INSERT 操作。更新現有紀錄將改為發送 SQL UPDATE 操作。驗證通常會在這些命令發送到資料庫之前執行。如果任何驗證失敗,該物件將被標記為無效,而 Active Record 將不會執行 INSERTUPDATE 操作。這避免在資料庫中儲存無效的物件。您可以選擇在物件建立、儲存或更新時執行特定的驗證。

有很多方法可以更改資料庫中物件的狀態。有些方法會觸發驗證,但有些則不會。這表示如果您不小心,可能會將物件儲存在資料庫中,但其狀態無效。

以下方法會觸發驗證,並且只有在物件有效時才會將物件儲存到資料庫

驚嘆號版本(例如 save!)會在紀錄無效時引發例外。非驚嘆號版本則不會:saveupdate 會傳回 false,而 create 會傳回物件。

1.3 略過驗證

以下方法會略過驗證,並將物件儲存到資料庫,而不論其有效性為何。應謹慎使用它們。請參閱方法文件以了解更多資訊。

請注意,如果將 validate: false 作為引數傳遞,save 也具有略過驗證的功能。此技術應謹慎使用。

  • save(validate: false)

1.4 valid?invalid?

在儲存 Active Record 物件之前,Rails 會執行您的驗證。如果這些驗證產生任何錯誤,Rails 將不會儲存該物件。

您也可以自行執行這些驗證。valid? 會觸發您的驗證,如果物件中沒有發現錯誤,則傳回 true,否則傳回 false。如您在上方看到的

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

在 Active Record 執行驗證後,任何失敗都可以透過 errors 實例方法存取,該方法會回傳一個錯誤集合。根據定義,如果一個物件在執行驗證後,這個集合是空的,則該物件為有效。

請注意,即使一個用 new 初始化的物件在技術上是無效的,也不會回報錯誤,因為只有在物件被儲存時(例如使用 createsave 方法),才會自動執行驗證。

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> p = Person.new
=> #<Person id: nil, name: nil>
irb> p.errors.size
=> 0

irb> p.valid?
=> false
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p = Person.create
=> #<Person id: nil, name: nil>
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p.save
=> false

irb> p.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

irb> Person.create!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

invalid?valid? 的反向。它會觸發您的驗證,如果物件中發現任何錯誤,則回傳 true,否則回傳 false。

1.5 errors[]

要驗證物件的特定屬性是否有效,您可以使用 errors[:attribute]。它會回傳 :attribute 的所有錯誤訊息陣列。如果指定的屬性沒有錯誤,則會回傳空陣列。

此方法僅在驗證執行後才有用,因為它只會檢查錯誤集合,而不會觸發驗證本身。它與上面解釋的 ActiveRecord::Base#invalid? 方法不同,因為它不會驗證整個物件的有效性。它只會檢查物件的個別屬性上是否發現錯誤。

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.new.errors[:name].any?
=> false
irb> Person.create.errors[:name].any?
=> true

我們將在「處理驗證錯誤」章節中更深入地探討驗證錯誤。

2 驗證輔助方法

Active Record 提供了許多預定義的驗證輔助方法,您可以直接在類別定義中使用。這些輔助方法提供了常見的驗證規則。每次驗證失敗時,都會將錯誤新增到物件的 errors 集合中,並且與正在驗證的屬性相關聯。

每個輔助方法都接受任意數量的屬性名稱,因此您可以使用一行程式碼將相同的驗證新增到多個屬性。

它們都接受 :on:message 選項,分別定義何時應執行驗證,以及如果驗證失敗,應將哪個訊息新增到 errors 集合中。:on 選項接受 :create:update 的其中一個值。每個驗證輔助方法都有一個預設的錯誤訊息。當未指定 :message 選項時,會使用這些訊息。讓我們看看每個可用的輔助方法。

要查看可用預設輔助方法的清單,請查看 ActiveModel::Validations::HelperMethods

2.1 acceptance

此方法驗證在提交表單時,使用者介面上的核取方塊是否已勾選。這通常用於當使用者需要同意應用程式的服務條款、確認已閱讀某些文字或任何類似的概念時。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

只有當 terms_of_service 不是 nil 時,才會執行此檢查。此輔助方法的預設錯誤訊息為「必須接受」。您也可以透過 message 選項傳入自訂訊息。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { message: "must be abided" }
end

它也可以接收 :accept 選項,該選項決定哪些允許的值會被視為可接受的。它預設為 ['1', true],並且可以輕鬆更改。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { accept: "yes" }
  validates :eula, acceptance: { accept: ["TRUE", "accepted"] }
end

此驗證非常特定於 Web 應用程式,並且此「接受」不需要記錄在資料庫中的任何地方。如果沒有對應的欄位,輔助方法會建立一個虛擬屬性。如果該欄位確實存在於資料庫中,則 accept 選項必須設定為或包含 true,否則驗證將不會執行。

2.2 confirmation

當您有兩個應該接收完全相同內容的文字欄位時,您應該使用此輔助方法。例如,您可能想要確認電子郵件地址或密碼。此驗證會建立一個虛擬屬性,其名稱是必須確認的欄位名稱,並附加 "_confirmation"。

class Person < ApplicationRecord
  validates :email, confirmation: true
end

在您的檢視範本中,您可以使用類似以下的程式碼

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

只有當 email_confirmation 不是 nil 時,才會執行此檢查。若要要求確認,請務必為確認屬性新增存在性檢查(我們稍後將在本指南中查看 presence )。

還有一個 :case_sensitive 選項,您可以使用它來定義確認約束是否區分大小寫。此選項預設為 true。

class Person < ApplicationRecord
  validates :email, confirmation: { case_sensitive: false }
end

此輔助方法的預設錯誤訊息為「與確認不符」。您也可以透過 message 選項傳入自訂訊息。

一般來說,當使用此驗證器時,您會希望將它與 :if 選項結合使用,以便僅在初始欄位已變更時驗證 "_confirmation" 欄位,而不是每次儲存記錄時都驗證。稍後會有關於條件驗證的更多資訊。

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true, if: :email_changed?
end

2.3 comparison

此檢查將驗證任何兩個可比較的值之間的比較。

class Promotion < ApplicationRecord
  validates :end_date, comparison: { greater_than: :start_date }
end

此輔助方法的預設錯誤訊息為「比較失敗」。您也可以透過 message 選項傳入自訂訊息。

這些選項都支援

  • :greater_than - 指定值必須大於提供的值。此選項的預設錯誤訊息為「必須大於 %{count}」
  • :greater_than_or_equal_to - 指定值必須大於或等於提供的值。此選項的預設錯誤訊息為「必須大於或等於 %{count}」
  • :equal_to - 指定值必須等於提供的值。此選項的預設錯誤訊息為「必須等於 %{count}」
  • :less_than - 指定值必須小於提供的值。此選項的預設錯誤訊息為「必須小於 %{count}」
  • :less_than_or_equal_to - 指定值必須小於或等於提供的值。此選項的預設錯誤訊息為「必須小於或等於 %{count}」
  • :other_than - 指定值必須與提供的值不同。此選項的預設錯誤訊息為「必須與 %{count} 不同」

驗證器需要提供比較選項。每個選項都接受值、proc 或符號。任何包含 Comparable 的類別都可以進行比較。

2.4 format

此輔助方法透過測試屬性的值是否與給定的正規表示式相符來驗證屬性的值,該正規表示式是使用 :with 選項指定的。

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "only allows letters" }
end

相反地,藉由使用 :without 選項,您可以要求指定的屬性與該正規表示式相符。

無論哪種情況,所提供的 :with:without 選項都必須是正規表示式,或是回傳正規表示式的 proc 或 lambda。

預設的錯誤訊息為「無效」

使用 \A\z 來比對字串的開頭和結尾,^$ 比對行的開頭/結尾。由於 ^$ 的頻繁誤用,如果您在提供的正規表示式中使用這兩個錨點中的任何一個,則需要傳遞 multiline: true 選項。在大多數情況下,您應該使用 \A\z

2.5 inclusion

此輔助方法驗證屬性的值是否包含在給定的集合中。事實上,這個集合可以是任何可列舉的物件。

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }
end

inclusion 輔助方法有一個選項 :in,它接收將被接受的值的集合。:in 選項有一個別名,稱為 :within,如果您願意,可以使用它來達到相同的目的。先前的範例使用 :message 選項來說明如何包含屬性的值。如需完整選項,請參閱訊息文件

此輔助方法的預設錯誤訊息為「未包含在清單中」

2.6 exclusion

inclusion 的相反是... exclusion

此輔助方法驗證屬性的值是否未包含在給定的集合中。事實上,這個集合可以是任何可列舉的物件。

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: %w(www us ca jp),
    message: "%{value} is reserved." }
end

exclusion 輔助方法有一個選項 :in,它接收驗證屬性將不接受的值的集合。:in 選項有一個別名,稱為 :within,如果您願意,可以使用它來達到相同的目的。此範例使用 :message 選項來說明如何包含屬性的值。如需訊息引數的完整選項,請參閱訊息文件

預設的錯誤訊息為「已保留」

除了傳統的可列舉物件(如 Array)之外,您還可以提供 proc、lambda 或回傳可列舉物件的符號。如果可列舉物件是數字、時間或日期時間範圍,則使用 Range#cover? 執行測試,否則使用 include?。當使用 proc 或 lambda 時,正在驗證的實例會作為引數傳遞。

2.7 length

此輔助方法驗證屬性值的長度。它提供了各種選項,因此您可以使用不同的方式指定長度限制

class Person < ApplicationRecord
  validates :name, length: { minimum: 2 }
  validates :bio, length: { maximum: 500 }
  validates :password, length: { in: 6..20 }
  validates :registration_number, length: { is: 6 }
end

可用的長度限制選項有

  • :minimum - 屬性不能少於指定的長度。
  • :maximum - 屬性不能多於指定的長度。
  • :in (或 :within)- 屬性長度必須包含在給定的間隔內。此選項的值必須是範圍。
  • :is - 屬性長度必須等於給定的值。

預設的錯誤訊息取決於正在執行的長度驗證類型。您可以使用 :wrong_length:too_long:too_short 選項,以及使用 %{count} 作為與正在使用的長度限制對應的數字的預留位置,來自訂這些訊息。您仍然可以使用 :message 選項來指定錯誤訊息。

class Person < ApplicationRecord
  validates :bio, length: { maximum: 1000,
    too_long: "%{count} characters is the maximum allowed" }
end

請注意,預設的錯誤訊息是複數形式(例如,「太短(最小值為 %{count} 個字元)」)。因此,當 :minimum 為 1 時,您應該提供自訂訊息或改用 presence: true。當 :in:within 的下限為 1 時,您應該提供自訂訊息或在 length 之前呼叫 presence

除了可以組合在一起的 :minimum:maximum 選項之外,一次只能使用一個限制選項。

2.8 numericality

此輔助方法驗證您的屬性是否只有數值。預設情況下,它將比對一個可選的符號,後面接著一個整數或浮點數。

若要指定只允許整數,請將 :only_integer 設定為 true。然後它將使用以下正規表示式來驗證屬性的值。

/\A[+-]?\d+\z/

否則,它將嘗試使用 Float 將值轉換為數字。Float 會使用欄位的精確度值或最多 15 位數轉換為 BigDecimal

class Player < ApplicationRecord
  validates :points, numericality: true
  validates :games_played, numericality: { only_integer: true }
end

:only_integer 的預設錯誤訊息為「必須是整數」

除了 :only_integer 之外,此輔助方法還接受 :only_numeric 選項,該選項指定值必須是 Numeric 的實例,並且如果它是 String,則會嘗試剖析該值。

預設情況下,numericality 不允許 nil 值。您可以使用 allow_nil: true 選項來允許它。請注意,對於 IntegerFloat 欄位,空字串會轉換為 nil

未指定任何選項時,預設的錯誤訊息為「不是數字」

還有許多選項可用於為可接受的值新增限制

  • :greater_than - 指定值必須大於提供的值。此選項的預設錯誤訊息為「必須大於 %{count}」
  • :greater_than_or_equal_to - 指定值必須大於或等於提供的值。此選項的預設錯誤訊息為「必須大於或等於 %{count}」
  • :equal_to - 指定值必須等於提供的值。此選項的預設錯誤訊息為「必須等於 %{count}」
  • :less_than - 指定值必須小於提供的值。此選項的預設錯誤訊息為「必須小於 %{count}」
  • :less_than_or_equal_to - 指定值必須小於或等於提供的值。此選項的預設錯誤訊息為「必須小於或等於 %{count}」
  • :other_than - 指定值必須與提供的值不同。此選項的預設錯誤訊息為「必須與 %{count} 不同」
  • :in - 指定值必須在提供的範圍內。此選項的預設錯誤訊息為「必須在 %{count} 中」
  • :odd - 指定值必須是奇數。此選項的預設錯誤訊息為「必須是奇數」
  • :even - 指定值必須是偶數。此選項的預設錯誤訊息為「必須是偶數」

2.9 presence

此輔助方法會驗證指定的屬性是否不為空。它使用 Object#blank? 方法來檢查值是否為 nil 或空白字串,也就是空字串或僅包含空白字元的字串。

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

如果您想確保關聯存在,您需要測試關聯的物件本身是否存在,而不是用於對應關聯的外鍵。這樣不僅會檢查外鍵是否不為空,還會檢查參照的物件是否存在。

class Supplier < ApplicationRecord
  has_one :account
  validates :account, presence: true
end

為了驗證需要存在的關聯記錄,您必須為關聯指定 :inverse_of 選項

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

如果您想確保關聯既存在又有效,您還需要使用 validates_associated。更多資訊請參閱下方

如果您驗證透過 has_onehas_many 關係關聯的物件是否存在,它會檢查該物件是否既不是 blank? 也不是 marked_for_destruction?

由於 false.blank? 為 true,如果您想驗證布林值欄位的存在,您應該使用以下其中一種驗證方式

# Value _must_ be true or false
validates :boolean_field_name, inclusion: [true, false]
# Value _must not_ be nil, aka true or false
validates :boolean_field_name, exclusion: [nil]

透過使用這些驗證方法之一,您將確保該值不會是 nil,這在大多數情況下會導致 NULL 值。

預設錯誤訊息為「can't be blank」(不能為空白)

2.10 absence

此輔助方法會驗證指定的屬性是否不存在。它使用 Object#present? 方法來檢查值是否既不是 nil 也不是空白字串,也就是空字串或僅包含空白字元的字串。

class Person < ApplicationRecord
  validates :name, :login, :email, absence: true
end

如果您想確保關聯不存在,您需要測試關聯的物件本身是否不存在,而不是用於對應關聯的外鍵。

class LineItem < ApplicationRecord
  belongs_to :order
  validates :order, absence: true
end

為了驗證需要不存在的關聯記錄,您必須為關聯指定 :inverse_of 選項

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

如果您想確保關聯既存在又有效,您還需要使用 validates_associated。更多資訊請參閱下方

如果您驗證透過 has_onehas_many 關係關聯的物件是否不存在,它會檢查該物件是否既不是 present? 也不是 marked_for_destruction?

由於 false.present? 為 false,如果您想驗證布林值欄位的不存在,您應該使用 validates :field_name, exclusion: { in: [true, false] }

預設錯誤訊息為「must be blank」(必須為空白)

2.11 uniqueness

此輔助方法會驗證屬性的值在物件儲存前是否唯一。

class Account < ApplicationRecord
  validates :email, uniqueness: true
end

驗證的方式是對模型的資料表執行 SQL 查詢,搜尋在該屬性中具有相同值的現有記錄。

有一個 :scope 選項,您可以使用它來指定一個或多個用於限制唯一性檢查的屬性

class Holiday < ApplicationRecord
  validates :name, uniqueness: { scope: :year,
    message: "should happen once per year" }
end

此驗證不會在資料庫中建立唯一性約束,因此可能會發生兩個不同的資料庫連線為您想要唯一的欄建立具有相同值的兩筆記錄。為了避免這種情況,您必須在資料庫中的該欄位上建立唯一索引。

為了在資料庫中新增唯一性資料庫約束,請在遷移中使用 add_index 陳述式,並包含 unique: true 選項。

如果您希望建立資料庫約束以防止使用 :scope 選項可能發生的唯一性驗證違規,您必須在資料庫的兩個欄位上建立唯一索引。有關多欄索引的更多詳細資訊,請參閱MySQL 手冊MariaDB 手冊,或參閱PostgreSQL 手冊以取得參照一組欄位的唯一約束範例。

還有一個 :case_sensitive 選項,您可以使用它來定義唯一性約束是否區分大小寫、不區分大小寫,或是否應遵守預設資料庫定序。此選項預設為遵守預設資料庫定序。

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

請注意,某些資料庫已設定為無論如何都會執行不區分大小寫的搜尋。

有一個 :conditions 選項,您可以將額外條件指定為 WHERE SQL 片段,以限制唯一性約束的查詢 (例如,conditions: -> { where(status: 'active') })。

預設錯誤訊息為「has already been taken」(已被使用)

如需更多資訊,請參閱 validates_uniqueness_of

2.12 validates_associated

當您的模型有始終需要驗證的關聯時,您應該使用此輔助方法。每次您嘗試儲存物件時,都會在每個關聯的物件上呼叫 valid?

class Library < ApplicationRecord
  has_many :books
  validates_associated :books
end

此驗證將適用於所有關聯類型。

請勿在關聯的兩端都使用 validates_associated。它們會彼此呼叫,形成無限迴圈。

validates_associated 的預設錯誤訊息為「is invalid」(無效)。請注意,每個關聯的物件都會包含其自己的 errors 集合;錯誤不會向上傳播到呼叫的模型。

validates_associated 只能用於 ActiveRecord 物件,到目前為止的所有內容也可以用於任何包含 ActiveModel::Validations 的物件。

2.13 validates_each

此輔助方法會針對區塊驗證屬性。它沒有預定義的驗證函式。您應該使用區塊建立一個,並且傳遞給 validates_each 的每個屬性都會針對它進行測試。

在以下範例中,我們將拒絕以小寫字母開頭的名字和姓氏。

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, "must start with upper case") if /\A[[:lower:]]/.match?(value)
  end
end

該區塊會接收記錄、屬性的名稱和屬性的值。

您可以在區塊內執行任何操作來檢查資料是否有效。如果您的驗證失敗,您應該向模型新增錯誤,因此使其無效。

2.14 validates_with

此輔助方法會將記錄傳遞給個別的類別進行驗證。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if record.first_name == "Evil"
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator
end

validates_with 沒有預設的錯誤訊息。您必須在驗證器類別中手動將錯誤新增至記錄的錯誤集合中。

新增至 record.errors[:base] 的錯誤與整個記錄的狀態有關。

若要實作 validate 方法,您必須在方法定義中接受一個 record 參數,該參數是要驗證的記錄。

如果您想在特定屬性上新增錯誤,請將其作為第一個引數傳遞,例如 record.errors.add(:first_name, "please choose another name")。我們稍後將更詳細地介紹驗證錯誤

def validate(record)
  if record.some_field != "acceptable"
    record.errors.add :some_field, "this field is unacceptable"
  end
end

validates_with 輔助方法會採用一個類別或要用於驗證的類別清單。

class Person < ApplicationRecord
  validates_with MyValidator, MyOtherValidator, on: :create
end

與所有其他驗證一樣,validates_with 接受 :if:unless:on 選項。如果您傳遞任何其他選項,它會將這些選項以 options 的形式傳送給驗證器類別

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any? { |field| record.send(field) == "Evil" }
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

請注意,驗證器只會在整個應用程式生命週期中初始化一次,而不是在每次驗證執行時,因此請小心在其中使用實例變數。

如果您的驗證器夠複雜,以至於您想要實例變數,您可以輕鬆改用普通的 Ruby 物件

class Person < ApplicationRecord
  validate do |person|
    GoodnessValidator.new(person).validate
  end
end

class GoodnessValidator
  def initialize(person)
    @person = person
  end

  def validate
    if some_complex_condition_involving_ivars_and_private_methods?
      @person.errors.add :base, "This person is evil"
    end
  end

  # ...
end

我們稍後將更詳細地介紹自訂驗證

3 常見的驗證選項

我們剛剛介紹的驗證器支援幾個常見選項,現在讓我們來介紹一些!

並非所有驗證器都支援所有這些選項,請參閱 ActiveModel::Validations 的 API 文件。

透過使用我們剛才提到的任何驗證方法,還有一個與驗證器共用的常見選項清單。我們現在將介紹這些!

  • :allow_nil:如果屬性為 nil,則跳過驗證。
  • :allow_blank:如果屬性為空白,則跳過驗證。
  • :message:指定自訂錯誤訊息。
  • :on:指定此驗證有效的情境。
  • :strict:驗證失敗時擲回例外。
  • :if:unless:指定應在何時發生或不應發生驗證。

3.1 :allow_nil

當正在驗證的值為 nil 時,:allow_nil 選項會跳過驗證。

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }, allow_nil: true
end
irb> Coffee.create(size: nil).valid?
=> true
irb> Coffee.create(size: "mega").valid?
=> false

有關訊息引數的完整選項,請參閱訊息文件

3.2 :allow_blank

:allow_blank 選項與 :allow_nil 選項類似。如果屬性的值為 blank?,例如 nil 或空字串,則此選項會讓驗證通過。

class Topic < ApplicationRecord
  validates :title, length: { is: 5 }, allow_blank: true
end
irb> Topic.create(title: "").valid?
=> true
irb> Topic.create(title: nil).valid?
=> true

3.3 :message

如您所見,:message 選項可讓您指定驗證失敗時將新增至 errors 集合的訊息。未使用此選項時,Active Record 會使用每個驗證輔助方法的各自預設錯誤訊息。

:message 選項接受 StringProc 作為其值。

String :message 值可以選擇性地包含 %{value}%{attribute}%{model} 的任何/所有值,這些值會在驗證失敗時動態取代。此取代是使用 i18n gem 完成的,並且預留位置必須完全相符,不允許有空格。

class Person < ApplicationRecord
  # Hard-coded message
  validates :name, presence: { message: "must be given please" }

  # Message with dynamic attribute value. %{value} will be replaced
  # with the actual value of the attribute. %{attribute} and %{model}
  # are also available.
  validates :age, numericality: { message: "%{value} seems wrong" }
end

Proc :message 值會獲得兩個引數:正在驗證的物件和具有 :model:attribute:value 鍵值對的雜湊。

class Person < ApplicationRecord
  validates :username,
    uniqueness: {
      # object = person object being validated
      # data = { model: "Person", attribute: "Username", value: <username> }
      message: ->(object, data) do
        "Hey #{object.name}, #{data[:value]} is already taken."
      end
    }
end

3.4 :on

:on 選項可讓您指定應在何時發生驗證。所有內建驗證輔助方法的預設行為是在儲存時執行 (無論是在建立新記錄時還是更新記錄時)。如果您想變更它,您可以使用 on: :create 僅在新建立記錄時執行驗證,或使用 on: :update 僅在更新記錄時執行驗證。

class Person < ApplicationRecord
  # it will be possible to update email with a duplicated value
  validates :email, uniqueness: true, on: :create

  # it will be possible to create the record with a non-numerical age
  validates :age, numericality: true, on: :update

  # the default (validates on both create and update)
  validates :name, presence: true
end

您也可以使用 on: 來定義自訂情境。自訂情境需要透過將情境的名稱傳遞給 valid?invalid?save 來明確觸發。

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
end
irb> person = Person.new(age: 'thirty-three')
irb> person.valid?
=> true
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"]}

person.valid?(:account_setup) 會執行驗證,但不會儲存模型。person.save(context: :account_setup) 會在儲存之前驗證 account_setup 情境中的 person

也可以接受符號陣列。

class Book
  include ActiveModel::Validations

  validates :title, presence: true, on: [:update, :ensure_title]
end
irb> book = Book.new(title: nil)
irb> book.valid?
=> true
irb> book.valid?(:ensure_title)
=> false
irb> book.errors.messages
=> {:title=>["can't be blank"]}

透過明確的情境觸發時,會針對該情境執行驗證,以及針對沒有情境的任何驗證。

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
  validates :name, presence: true
end
irb> person = Person.new
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]}

我們將在 callbacks 指南中介紹更多 on: 的使用案例。

4 嚴格驗證

您也可以指定驗證是嚴格的,並在物件無效時擲回 ActiveModel::StrictValidationFailed

class Person < ApplicationRecord
  validates :name, presence: { strict: true }
end
irb> Person.new.valid?
ActiveModel::StrictValidationFailed: Name can't be blank

也可以將自訂例外傳遞給 :strict 選項。

class Person < ApplicationRecord
  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
irb> Person.new.valid?
TokenGenerationException: Token can't be blank

5 條件式驗證

有時只有在滿足給定的述詞時驗證物件才有意義。您可以使用 :if:unless 選項來達成此目的,這些選項可以採用符號、ProcArray。當您想指定驗證應該在何時發生時,可以使用 :if 選項。或者,如果您想指定驗證不應該在何時發生時,則可以使用 :unless 選項。

5.1 將符號與 :if:unless 搭配使用

您可以將 :if:unless 選項與符號關聯,該符號對應於驗證發生之前會呼叫的方法名稱。這是最常用的選項。

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

5.2 將 Proc 與 :if:unless 搭配使用

可以將 :if:unless 與一個會被呼叫的 Proc 物件關聯。使用 Proc 物件讓您能夠撰寫內聯條件,而不是單獨的方法。此選項最適合用於單行程式碼。

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

由於 lambda 是一種 Proc,因此它也可以利用簡短的語法來撰寫內聯條件。

validates :password, confirmation: true, unless: -> { password.blank? }

5.3 群組條件驗證

有時候,讓多個驗證使用同一個條件會很有用。這可以透過使用 with_options 輕鬆實現。

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

with_options 區塊內的所有驗證都會自動通過 if: :is_admin? 條件。

5.4 組合驗證條件

另一方面,當多個條件決定是否應執行驗證時,可以使用 Array。此外,您可以將 :if:unless 都應用於同一個驗證。

class Computer < ApplicationRecord
  validates :mouse, presence: true,
                    if: [Proc.new { |c| c.market.retail? }, :desktop?],
                    unless: Proc.new { |c| c.trackpad.present? }
end

只有當所有 :if 條件評估為 true 且沒有任何 :unless 條件評估為 true 時,才會執行驗證。

6 執行自訂驗證

當內建的驗證輔助方法不足以滿足您的需求時,您可以根據您的喜好撰寫自己的驗證器或驗證方法。

6.1 自訂驗證器

自訂驗證器是繼承自 ActiveModel::Validator 的類別。這些類別必須實作 validate 方法,該方法接收一個記錄作為引數並對其執行驗證。自訂驗證器會使用 validates_with 方法呼叫。

class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.start_with? "X"
      record.errors.add :name, "Provide a name starting with X, please!"
    end
  end
end

class Person < ApplicationRecord
  validates_with MyValidator
end

為驗證個別屬性新增自訂驗證器的最簡單方法是使用方便的 ActiveModel::EachValidator。在這種情況下,自訂驗證器類別必須實作一個 validate_each 方法,該方法接收三個引數:記錄、屬性和值。這些引數分別對應到實例、要驗證的屬性以及傳入實例中該屬性的值。

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      record.errors.add attribute, (options[:message] || "is not an email")
    end
  end
end

class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

如範例所示,您也可以將標準驗證與您自己的自訂驗證器組合使用。

6.2 自訂方法

您也可以建立用於驗證模型狀態的方法,並在模型無效時將錯誤新增至 errors 集合。然後,您必須透過使用 validate 類別方法來註冊這些方法,並傳入驗證方法名稱的符號。

您可以為每個類別方法傳入多個符號,並且各自的驗證將以它們註冊的相同順序執行。

valid? 方法將驗證 errors 集合是否為空,因此您的自訂驗證方法應在您希望驗證失敗時將錯誤新增至該集合。

class Invoice < ApplicationRecord
  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.present? && 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

預設情況下,這些驗證會在您每次呼叫 valid? 或儲存物件時執行。但是,也可以透過為 validate 方法提供 :on 選項來控制何時執行這些自訂驗證,其選項為::create:update

class Invoice < ApplicationRecord
  validate :active_customer, on: :create

  def active_customer
    errors.add(:customer_id, "is not active") unless customer.active?
  end
end

有關 :on 的更多詳細資訊,請參閱上面的章節。

6.3 列出驗證器

如果您想找出給定物件的所有驗證器,請直接使用 validators

例如,如果我們有以下使用自訂驗證器和內建驗證器的模型

class Person < ApplicationRecord
  validates :name, presence: true, on: :create
  validates :email, format: URI::MailTo::EMAIL_REGEXP
  validates_with MyOtherValidator, strict: true
end

我們現在可以使用「Person」模型上的 validators 來列出所有驗證器,甚至可以使用 validators_on 來檢查特定欄位。

irb> Person.validators
#=> [#<ActiveRecord::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={:on=>:create}>,
     #<MyOtherValidatorValidator:0x10b2f17d0
      @attributes=[:name], @options={:strict=>true}>,
     #<ActiveModel::Validations::FormatValidator:0x10b2f0f10
      @attributes=[:email],
      @options={:with=>URI::MailTo::EMAIL_REGEXP}>]
     #<MyOtherValidator:0x10b2f0948 @options={:strict=>true}>]

irb> Person.validators_on(:name)
#=> [#<ActiveModel::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={on: :create}>]

7 使用驗證錯誤

valid?invalid? 方法僅提供有效性的摘要狀態。但是,您可以透過使用 errors 集合中的各種方法來深入瞭解每個個別錯誤。

以下是最常用的方法清單。有關所有可用方法的清單,請參閱 ActiveModel::Errors 文件。

7.1 errors

您可以用來深入瞭解每個錯誤各種詳細資訊的閘道。

這會傳回一個包含所有錯誤的 ActiveModel::Errors 類別的實例,每個錯誤都由一個 ActiveModel::Error 物件表示。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.full_messages
=> ["Name can't be blank", "Name is too short (minimum is 3 characters)"]

irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors.full_messages
=> []

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.first.details
=> {:error=>:too_short, :count=>3}

7.2 errors[]

當您想要檢查特定屬性的錯誤訊息時,可以使用 errors[]。它會傳回一個字串陣列,其中包含給定屬性的所有錯誤訊息,每個字串包含一個錯誤訊息。如果沒有與該屬性相關的錯誤,則會傳回一個空陣列。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors[:name]
=> []

irb> person = Person.new(name: "JD")
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["is too short (minimum is 3 characters)"]

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["can't be blank", "is too short (minimum is 3 characters)"]

7.3 errors.where 和錯誤物件

有時候,我們可能需要每個錯誤的更多資訊,而不僅僅是其訊息。每個錯誤都封裝為一個 ActiveModel::Error 物件,而 where 方法是最常見的存取方式。

where 會傳回一個錯誤物件陣列,該陣列會依各種條件程度篩選。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end

我們可以只篩選 attribute,方法是將其作為第一個參數傳遞給 errors.where(:attr)。第二個參數用於篩選我們想要的錯誤 type,方法是呼叫 errors.where(:attr, :type)

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name)
=> [ ... ] # all errors for :name attribute

irb> person.errors.where(:name, :too_short)
=> [ ... ] # :too_short errors for :name attribute

最後,我們可以依給定類型的錯誤物件上可能存在的任何 options 進行篩選。

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name, :too_short, minimum: 3)
=> [ ... ] # all name errors being too short and minimum is 2

您可以從這些錯誤物件讀取各種資訊

irb> error = person.errors.where(:name).last

irb> error.attribute
=> :name
irb> error.type
=> :too_short
irb> error.options[:count]
=> 3

您也可以產生錯誤訊息

irb> error.message
=> "is too short (minimum is 3 characters)"
irb> error.full_message
=> "Name is too short (minimum is 3 characters)"

full_message 方法會產生一個更使用者友善的訊息,並預先加上大寫的屬性名稱。(若要自訂 full_message 使用的格式,請參閱 I18n 指南。)

7.4 errors.add

add 方法會透過接收 attribute、錯誤 type 和額外的選項雜湊來建立錯誤物件。當您撰寫自己的驗證器時,這會很有用,因為它可讓您定義非常特定的錯誤情況。

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_plain, message: "is not cool enough"
  end
end
irb> person = Person.create
irb> person.errors.where(:name).first.type
=> :too_plain
irb> person.errors.where(:name).first.full_message
=> "Name is not cool enough"

7.5 errors[:base]

您可以新增與物件整體狀態相關的錯誤,而不是與特定屬性相關的錯誤。若要執行此操作,您必須在新增新錯誤時使用 :base 作為屬性。

class Person < ApplicationRecord
  validate do |person|
    errors.add :base, :invalid, message: "This person is invalid because ..."
  end
end
irb> person = Person.create
irb> person.errors.where(:base).first.full_message
=> "This person is invalid because ..."

7.6 errors.size

size 方法會傳回物件的錯誤總數。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.size
=> 2

irb> person = Person.new(name: "Andrea", email: "andrea@example.com")
irb> person.valid?
=> true
irb> person.errors.size
=> 0

7.7 errors.clear

當您有意要清除 errors 集合時,可以使用 clear 方法。當然,在無效物件上呼叫 errors.clear 並不會使其有效:errors 集合現在會是空的,但是當您下次呼叫 valid? 或任何嘗試將此物件儲存到資料庫的方法時,驗證將會再次執行。如果任何驗證失敗,errors 集合將會再次填滿。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.empty?
=> false

irb> person.errors.clear
irb> person.errors.empty?
=> true

irb> person.save
=> false

irb> person.errors.empty?
=> false

8 在檢視中顯示驗證錯誤

建立模型並新增驗證後,如果該模型是透過網頁表單建立的,您可能希望在其中一個驗證失敗時顯示錯誤訊息。

由於每個應用程式處理此類事情的方式都不同,因此 Rails 沒有包含任何檢視輔助方法來協助您直接產生這些訊息。但是,由於 Rails 為您提供了許多與驗證互動的方法,因此您可以建立自己的方法。此外,當產生 scaffold 時,Rails 會將一些 ERB 放入它產生的 _form.html.erb 中,其中會顯示該模型上的完整錯誤清單。

假設我們有一個儲存在名為 @article 的實例變數中的模型,其外觀如下

<% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

    <ul>
      <% @article.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

此外,如果您使用 Rails 表單輔助方法來產生您的表單,則當欄位發生驗證錯誤時,它會在條目周圍產生一個額外的 <div>

<div class="field_with_errors">
  <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>

然後,您可以根據自己的喜好設定此 div 的樣式。例如,Rails 產生的預設 scaffold 會新增此 CSS 規則

.field_with_errors {
  padding: 2px;
  background-color: red;
  display: table;
}

這表示任何有錯誤的欄位都會以 2 個像素的紅色邊框結束。



返回頂端