[Rails]Model驗證中常見的重要觀念
一般來說,將資料存入資料庫前,為了確保資料的有效性,通常都會透過驗證來確保只有有效的資料才能存入資料庫。
雖然前端通常也會透過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 驗證及回呼