[Rails]Model驗證中常見的重要觀念

in #cn6 years ago


一般來說,將資料存入資料庫前,為了確保資料的有效性,通常都會透過驗證來確保只有有效的資料才能存入資料庫。

雖然前端通常也會透過HTML和JS進行驗證,但難保不會有人關掉JS、會自行重新建立一組相似的表單。

而在Model層級驗證資料是最好的,只有通過驗證的資料方可存入資料庫。因為在 Model 層面驗證,不需要考慮資料庫的種類、無法在用戶端(瀏覽器)跳過驗證、且更容易測試與維護。

以下會針對一些驗證常見的重要觀念解釋。像是觸發驗證時機、會與不會觸發驗證的方法、如何手動加入錯誤訊息等等。

驗證觸發時機

Rails的驗證基本上只會在物件要被儲存、或是更動到資料庫時才會被觸發。

也就是說,當我們新增了一個Product的物件,但是還沒存到資料庫時,是不會觸發驗證的。因為這個時候此物件還不屬於資料庫,並不會觸發驗證。

如果我們想知道一個物件是否已經被存入資料庫中,可以透過new_record?這個方法。

product = Product.new
product.new_recored?
=> true
product.save
product.new_record?
=> false

會觸發驗證的方法

以下幾個方法會觸發驗證

create
create!
save
save!
update
update!

另外值得注意的是,使用有Bang(!)的方法時,如果資料無效會拋出Exception(異常);沒有Bang的則不會

不會觸發驗證的方法

以下的方法不會觸發驗證,將資料存入資料庫的時候不會考慮資料的有效性

decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters

自行觸發驗證

如果平時要自己觸發驗證,而不想要將資料存到資料庫中,可以透過valid?或是invalid?來觸發。

如果資料是有效的,valid?會回傳true;反之亦然。而invalid?的情況跟valid?完全相反。

errors(驗證錯誤訊息)

errors這個方法會回傳一個ActiveModel::Errors 類別的實體,包含了所有的錯誤。屬性名稱為鍵,值為由錯誤訊息字串組成的陣列。

p = Product.new
p.errors
=> #<ActiveModel::Errors:0x007faae10f6368 @base=#<Product id: nil, user: nil, template: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}>

errors.messages(得到所有錯誤訊息)

當驗證物件出現錯誤時,所有的錯誤訊息可以在使用errors.message這個方法來取得。

class Product < ActiveRecord::Base
  validates :title, presence: true
  validates :sku, length: { minimum: 2 }
end

p = Product.create(sku: "ab")
(0.1ms)  begin transaction
(0.2ms)  rollback transaction
=> #<Product id: nil, title: nil, sku: nil, template: nil, created_at: nil, updated_at: nil>

p.errors.messages
=> {:title=>["can't be blank"], :sku=>["is too short (minimum is 4 characters)"]}

需要注意的是,當你執行new的方法產生實體時,即使有錯誤也不會出現在錯誤訊息中,因為new並不觸發驗證。

class Product < ActiveRecord::Base
  validates :title, presence: true
end

product = Product.new
product.errors.messages
=> {}

product.valid? # => false
product.errors.messages
=> {:title=>["can't be blank"]}

errors[:attribute](得到特定屬性的錯誤訊息)

errors[:attribute]用來得到特定屬性的錯誤訊息。如果該屬性沒有錯誤訊息,則回傳空陣列

class Product < ActiveRecord::Base
  validates :title, presence: true
  validates :sku, length: { minimum: 2 }
end

p = Product.create(title: "books", sku: "ab")
p.errors[:sku]
=> ["is too short (minimum is 4 characters)"]
p.errors[:title]
=> []

errors.add(手動加入特定屬性的錯誤訊息)

平時在驗證時如果物件無效,會產生預設的錯誤訊息。如果我們想要自己手動加入針對特定屬性的錯誤訊息,我們可以過erros.add這個方法幫我們達成目的。

使用方法是第一個參數為屬性、第二個參數為手動加入的錯誤訊息。

以下我們會透過自行設定一個驗證方法來呈現。

class Product < ApplicationRecord
  validate :title_validator

  private
    def title_validator
      unless title.present?
        errors.add(:title, "Title不能是空的喔!")
      end
    end
end
product = Product.create
(0.1ms)  begin transaction
(0.1ms)  rollback transaction
 => #<Order id: nil, title: nil, created_at: nil, updated_at: nil>
product.errors.messages
 => {:title=>["Title不能是空的喔!"]}

又或者你可以透過<<將錯誤訊息加入屬性中

class Product < ApplicationRecord
  validate :title_validator

  private
    def title_validator
      unless title.present?
        errors[:title] << "Title不能是空的喔!"
      end
    end
end

product = Product.create
(0.1ms)  begin transaction
(0.1ms)  rollback transaction
=> #<Order id: nil, title: nil, created_at: nil, updated_at: nil>
product.errors.messages
=> {:title=>["Title不能是空的喔!"]}

errors[:base](為整個物件加入錯誤訊息,不針對某個屬性)

如果我們想為整個物件加入錯誤訊息,可以透過errors[:base]來加入。你同樣可以使用add<<的方式來達成。

class Product < ApplicationRecord
  validate :title_validator

  private
    def title_validator
      unless title.present?
        errors[:base] << "測試錯誤訊息")
        # errors.add(:base, "測試錯誤訊息")
      end
    end
end

product = Product.create
(0.1ms)  begin transaction
(0.1ms)  rollback transaction
=> #<Order id: nil, title: nil, created_at: nil, updated_at: nil>
product.errors[:base]
=> ["測試錯誤訊息"]

errors.clear(清除所有錯誤訊息)

使用errors.clear 方法可以清除 errors 集合裡的所有錯誤。不過這個過程並不涉及任何驗證過程,也不會改變一個物件的有效性。

當下一次觸發驗證時,如果有錯誤訊息仍舊會重新填入errors集合。

class Product < ActiveRecord::Base
  validates :title, presence: true
  validates :sku, length: { minimum: 2 }
end

p = Product.create(title: "books", sku: "ab")
p.errors.messages
=> {:sku=>["is too short (minimum is 4 characters)"] }
p.errors.clear
=> {}
p.errors.messages
=> {}
p.save
p.errors.messages
=> {:sku=>["is too short (minimum is 4 characters)"] }

自訂驗證方法

我們在上面的文章中便已經有實作過自訂驗證方法,一般的做法是定義一個private method,並透過validate來註冊這個method

class Invoice < ActiveRecord::Base
  validate :expiration_date_cannot_be_in_the_past
    
    
  private
    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
end

常見輔助驗證方法

在Rails中有許多常見的輔助驗證方法,像是

acceptance
presence
congirmation
format
...等等

看更多輔助驗證方法可以參考Validation Helpers

參考資料
Active Record Validations
為你自己學為你自己學 Ruby on Rails — Model 驗證及回呼