v7.1.3.2
更多資訊請參閱 rubyonrails.org: 更多 Ruby on Rails

Active Record 驗證

本指南教您如何使用 Active Record 的驗證功能,在物件進入資料庫之前驗證其狀態。

閱讀本指南後,您將了解

1 驗證概述

以下是極為簡單的驗證範例

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

如您所見,我們的驗證讓我們知道我們的 Person 在沒有 name 屬性的情況下是不合法的。第二個 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 作業。這可以避免將無效物件儲存在資料庫中。您可以選擇在建立、儲存或更新物件時執行特定驗證。

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

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

  • create
  • create!
  • save
  • save!
  • update
  • update!

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

1.3 略過驗證

下列方法會略過驗證,並會將物件儲存至資料庫,而不論其有效性。應謹慎使用這些方法。

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • insert
  • insert!
  • insert_all
  • insert_all!
  • toggle!
  • touch
  • touch_all
  • update_all
  • update_attribute
  • update_column
  • update_columns
  • update_counters
  • upsert
  • upsert_all

請注意,如果傳遞 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

這個驗證非常特定於網路應用程式,而且這個「接受」不需要記錄在你的資料庫中。如果你沒有它的欄位,這個輔助函式將會建立一個虛擬屬性。如果欄位存在於你的資料庫中,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

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

還有一個 :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} 不同」

驗證器需要提供比較選項。每個選項都接受值、程序或符號。任何包含 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 選項都必須是正規表示式或會傳回正規表示式的程序或 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 選項來展示你如何包含屬性的值。有關訊息引數的完整選項,請參閱 訊息文件

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

除了傳統的可列舉(例如陣列),你還可以提供一個返回可列舉的程序、lambda 或符號。如果可列舉是一個數字、時間或日期時間範圍,則使用 Range#cover? 執行測試,否則使用 include?。在使用程序或 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 值。

預設錯誤訊息為「不能為空白」

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] }

預設錯誤訊息為 「必須為空白」

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 手冊 以取得有關多欄位索引的更多詳細資訊,或參閱 PostgreSQL 手冊 以取得參考欄位群組的唯一約束範例。

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

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

請注意,某些資料庫的設定會執行不區分大小寫的搜尋。

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

預設錯誤訊息為 「已使用」

請參閱 validates_uniqueness_of 以取得更多資訊。

2.12 validates_associated

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

class Library < ApplicationRecord
  has_many :books
  validates_associated :books
end

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

請勿在關聯的兩端都使用 validates_associated。它們會在無限迴圈中互相呼叫。

validates_associated 的預設錯誤訊息為 「無效」。請注意,每個關聯物件都包含其自己的 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] 的錯誤與記錄整體的狀態有關。

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

如果您想針對特定屬性新增錯誤,請將其傳遞為第一個引數,例如 record.errors.add(:first_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

:allow_nil 選項會在驗證值為 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"]}

我們將在 callback 指南 中介紹更多 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

由於 lambdaProc 的一種,因此也可以用來撰寫內嵌條件,並利用簡短的語法。

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 條件都成立,且沒有任何 :unless 條件成立時,驗證才會執行。

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? 或儲存物件時執行。但也可以透過提供 :on 選項給 validate 方法來控制執行這些自訂驗證的時機,選項包括::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) 的第一個參數來篩選。第二個參數用於透過呼叫 errors.where(:attr, :type) 來篩選我們想要的錯誤 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: "[email protected]")
irb> person.valid?
=> true
irb> person.errors.size
=> 0

7.7 errors.clear

clear 方法用於您有意要清除 errors 集合時。當然,對無效物件呼叫 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 提供許多方法讓您與驗證進行互動,因此您可以建立自己的驗證。此外,在產生架構時,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 產生的預設架構會加入這個 CSS 規則

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

這表示任何有錯誤的欄位最後都會有一個 2 像素的紅色邊框。

意見回饋

我們鼓勵您協助提升本指南的品質。

如果您看到任何錯字或事實錯誤,請協助我們修正。若要開始,您可以閱讀我們的 文件貢獻 章節。

您也可能會發現不完整或過時的內容。請務必為 main 新增任何遺漏的文件。請務必先查看 Edge Guides,以驗證問題是否已在 main 分支中修正。請查看 Ruby on Rails Guides 指南 以了解樣式和慣例。

如果您發現需要修正的地方,但無法自行修正,請 開啟問題

最後但並非最不重要的,任何有關 Ruby on Rails 文件的討論都非常歡迎在 官方 Ruby on Rails 論壇 中進行。