Custom Validators in Ruby on Rails
Bài đăng này đã không được cập nhật trong 7 năm
1. Validations Overview
Mình nói qua về Validator thông qua 1 ví dụ nho nhỏ như sau:
class User < ApplicationRecord
validates :Tel, presence: true
end
User.create(tel: "0969696969").valid? # => true
User.create(tel: nil).valid? # => false
Có thể hiểu đơn giản nhất Validates là các thao tác kiểm tra dữ liệu trước khi lưu 1 object vào DB
2. Validations Helper
Trong Ruby on Rails thì ActiveRecord có sẵn rất nhiều các helper để trợ giúp validation. Chúng ta có thể kể ra 1 vài helper mà RoR hỗ trợ như: presence, absence, uniqueness, confirmation ... Để tìm hiểu thêm về các helper này chúng ta có thể tham khảo trên trang chủ Ruby on Rails hoặc 1 vài bài viết về Validations trên viblo: http://guides.rubyonrails.org/active_record_validations.html
3. Custom Validations
Có thể thấy việc Validations Trong Ruby on Rails được hỗ trợ rất mạnh, tuy nhiên trong thực tế, dựa theo yêu cầu nghiệp vụ của các dự án mà chúng ta gặp phải 1 số trường cần Validates theo cách đặc biệt. Khi đó thì việc Custom Validations là cần thiết
4. Ví dụ thực tế về Custom Validators
Ở đây mình sẽ đưa ra 1 bài toán nhỏ nhỏ như sau: Trường Tel trong bảng User khi nhập cần thỏa mã các yêu cầu sau: không được bỏ trống, là số, từ 10-11 số, cho phép nhập hyphens ("-") và space (khoảng trắng), không được có 2 hyphens hoặc 2 spaces liên tiếp và không được để hyphens, spaces cạnh nhau liên tiếp. Phân tích bài toán 1 chút mình thấy: về đầu bài toán "không được bỏ trống, là số, từ 10-11 số" thì khá đơn giản, nhưng nửa sau tương đối phức tạp. Cách đơn giản nhất mà mình nghĩ ra đầu tiên là viết 1 module để validation thẳng Tel rồi include vào model
app/models/concerns/verify_tel.rb
module VerifyTel
extend ActiveSupport::Concern
VALID_PHONE_REGEX = /\A[0-9\s-]+\z/
def verify_tel object, tel
phone = object.send(tel).gsub(/[\s-]/, "")
if object.send(tel).present?
if object.send(tel).last == "-" || object.send(tel).match(VALID_PHONE_REGEX).nil? ||
object.send(tel).include?(" ") || object.send(tel).include?("--") || (phone.size != 10 && phone.size != 11)
errors.add tel.to_sym, I18n.t("errors.messages.invalid")
end
end
end
end
\\Giải thích 1 chút là đầu tiên mình gsub để loại bỏ hyphesn và space trong input, sau đó mình if để kiểm tra sao cho thỏa mãn yêu cầu bài toàn rồi add errors
app/models/user.rb
include VerifyTel
validate :verify_phone_number
def verify_chophone_number
verify_tel self, "tel"
end
Về cơ bản cách này cũng tương đối ổn, nhưng nghĩ 1 chút thì thấy khi cần tái sử dụng việc validation này trong các model khác hoặc cho các trường khác là không thể. Chúng ta sẽ lại phải include, rồi khai báo các hàm 1 lần nữa, thật mệt mỏi --> Mình sẽ thử sử dụng Custom Validation mà ROR hỗ trợ nhé
app/validators/phone_number_validator.rb
class PhoneNumberValidator < ActiveModel::EachValidator
VALID_PHONE_REGEX = /\A[0-9\s-]+\z/
def validate_each record, attribute, value
phone = value.gsub /[\s-]/, ""
if value.last == "-" || value.match(VALID_PHONE_REGEX).nil? ||
value.include?("- ") || value.include?(" -") || value.include?(" ") ||
value.include?("--") || (phone.size != 10 && phone.size != 11)
record.errors[attribute] << (options[:messages] || I18n.t("errors.messages.invalid"))
end
end
end
\\ PhoneNumberValidator được kế thừa từ ActiveModel::EachValidator
app/models/user.rb
validates :tel, presence: true, phone_number: true
Có thể thấy việc được kế thừa từ ActiveModel::EachValidator thì PhoneNumberValidator được hiểu như 1 method được ROR hỗ trợ mặc định (giống như presence, absence, uniqueness ... mà mình kể ở trên) với cách này thì chúng ta có thể dễ dàng tái sự dụng cho các trường khác nhau hay ở các model khác nhau
Tài liệu tham khảo
http://guides.rubyonrails.org/active_record_validations.html http://www.rails-dev.com/custom-validators-in-ruby-on-rails-4/
All rights reserved