(Tôi gửi bài này cho mailing list vài tháng trước. Nếu thích, muốn đọc thêm, hãy đăng ký!)
Tôi gặp tình huống lạ vài ngày trước với app.
Giả sử model Article, bài viết tạo trước là draft. Draft nên nhẹ. Muốn tạo/sửa ngay, không cần body, thậm chí title.
Nhưng khi publish, cần validation thêm. Phải có title, body, và cái đống kia.
Bạn có thể viết đống if tự check. Nhưng mất form handling đẹp của Rails. Tôi thấy đi với Rails cho việc Rails làm tốt sẽ đỡ đau đầu sau. Vẫn có cách dùng validation cho việc này?
if và unless?
Validation hỗ trợ :if và :unless, nhưng không luôn sạch:
class Article < ActiveRecord::Base
# validate in draft mode
validates_presence_of :author_id
# validate only when published
validates_presence_of :body, unless: lambda { |o| o.draft? }
...
end
Ngoài ra, đây không phải cách tôi muốn publish article. Tôi muốn coi publish là hành động, không phải trạng thái article ở, nên set và check attribute không phải câu trả lời.
Tính năng Rails ít biết, ít document
Vấn đề này hoàn h hợp cho validation contexts của Rails. Tôi lần đầu biết custom validation contexts từ gist DHH viết. Nhưng khó tìm giải thích tốt chúng là gì, và dùng như thế nào.
Document cho custom contexts mỏng. Nhắc duy nhất tôi thấy trong API docs ở đây. Và API docs cho validation này không nhắc custom contexts chút nào! Làm khó đặc biệt – nếu không biết tên gọi, làm sao tìm docs thêm?
Cuối cùng tìm thấy overview tốt ở đây: https://blog.arkency.com/2014/04/mastering-rails-validations-contexts/. Đáng đọc. Xem ví dụ chúng ta:
class Article < ActiveRecord::Base
# validate in draft mode
validates_presence_of :author_id
# validate only when published
validates_presence_of :body, on: :publish
...
end
class ArticleController < ApplicationController
respond_to :html
def create
@article = Article.create(article_params)
respond_with @article
end
def publish
@article = Article.find(params[:id])
@article.attributes = article_params
@article.save(context: :publish)
respond_with @article
end
private
def article_params
params.require(:article).permit(:body, :title, :author_id)
end
end
Khá gọn! Chỉ lạ chỗ dùng save thay update trong publish. Vì update không nhận validation context param. (Có lẽ cơ hội pull request?)
Vượt qua save
Custom validation contexts dùng cho hơn save record khác cách. Contexts dùng với valid?:
@article.valid?(:publish)
vì vậy dùng validation contexts bất cứ nơi nào muốn trả lời: "Object này có property này không? Nếu không, tại sao?"
DHH tạo method kết thúc -able? delegate tới valid?(:context). Tôi rất thích kiểu đó:
class Article
validate :has_been_published, on: :view
validate :is_not_spam, on: :view
def viewable?
valid? :view
end
private
def has_been_published
if published_at.future?
errors.add(:published_at, "is in the future")
end
end
def is_not_spam
if is_spam?(body)
errors.add(:body, "has been detected as spam")
end
end
end
Cách này, khi check, bạn nhận info hơn true/false:
unless @article.viewable?
# @article.errors giờ đã đầy
end
Article viewable không? Vậy tại sao không?
Chỉ vừa đủ nhẹ
Có vài cách khác đạt validation behavior này. Custom ActiveModel service object với validation riêng. Dùng :if hoặc :unless trên validation. Nhưng như nhiều thứ Rails, custom validation contexts là sự thỏa hiệp tốt giữa readability và flexibility.