12/08/2018, 14:01

Mastering Rails Validations: Contexts

Bạn đã bao giờ nghĩ tới việc tùy chỉnh validate trong mỗi phân quyền trong Rails chưa? Đó là người sử dụng có quyền cao hơn được cấp quy tắc xác nhận ít nghiêm ngặt hơn. Bắt đầu nào class User < ActiveRecord::Base validates_length_of :slug, minimum: 3 end Nếu chúng ta muốn thêm ...

context_validation.jpg

Bạn đã bao giờ nghĩ tới việc tùy chỉnh validate trong mỗi phân quyền trong Rails chưa?
Đó là người sử dụng có quyền cao hơn được cấp quy tắc xác nhận ít nghiêm ngặt hơn.

Bắt đầu nào

class User < ActiveRecord::Base
  validates_length_of :slug, minimum: 3
end

Nếu chúng ta muốn thêm xác nhận validate đó sẽ khác nhau cho các quản trị viên và khác nhau cho người sử dụng thì làm thế nào.

Phương Pháp đầu tiên mà bạn sẽ nghĩ đến là gì ?

Ta sẽ tạo thêm một attr_accessor rồi tiến hành validate như thế này:

class User < ActiveRecord::Base
  attr_accessor: :edited_by_admin
  validates_length_of :slug, minimum: 3, unless: Proc.new{|u| u.edited_by_admin? }
  validates_length_of :slug, minimum: 1, if:     Proc.new{|u| u.edited_by_admin? }
end

class Admin::UsersController
  def edit
    @user = User.find(params[:id])
    @user.edited_by_admin = true
    if @user.save
      redirect # ...
    else
      render # ...
    end
  end
end

Và cách này sẽ hoạt động, tuy nhiên nó không phải là đoạn mã mà chúng ta có thể tự hào.

Bạn đã biết một cách để validate chỉ kích hoạt khi thực hiện các hành động khác nhau. Bạn có nhớ không?

class Meeting < ActiveRecord::Base
  validate :starts_in_future, on: :create
end

Validate này chỉ hoặt động khi chúng ta thực hiện create

Vậy liệu chúng ta có thể sử dụng nó ...

Và đây là cách mà chúng ta mong muốn

class User < ActiveRecord::Base
  validates_length_of :slug, minimum: 3, on: :user
  validates_length_of :slug, minimum: 1, on: :admin
end

class Admin::UsersController
  def edit
    @user = User.find(params[:id])
    if @user.save(context: :admin)
      redirect # ...
    else
      render # ...
    end
  end
end

Wow, bây giờ nhìn vào đó. Nó thật là dễ thương phải không?

Và nếu bạn chỉ muốn kiểm tra xác nhận mà không lưu các đối tượng bạn có thể sử dụng:

u = User.new
u.valid?(:admin)
u.valid?(:user)

Bây giờ là một thời điểm tốt để nhắc nhở mình về một API tốt đẹp mà có thể làm cho nó ít dư thừa trong trường hợp nhiều quy tắc:

class User < ActiveRecord::Base
  with_options({on: :user}) do |for_user|
    for_user.validates_length_of :slug, minimum: 3
    for_user.validates_acceptance_of :terms_of_service
  end

  with_options({on: :admin}) do |for_admin|
    for_admin.validates_length_of :slug, minimum: 1
  end
end

Vấn đề với cách tiếp cận này là bạn không thể cung cấp nhiều ngữ cảnh.

Nếu bạn muốn có một validate on: :admin và thêm một số on: :create thì sao

Ví dụ bạn validate như thế này:

class User < ActiveRecord::Base
  validates_length_of :slug, minimum: 3, on: :user
  validates_length_of :slug, minimum: 1, on: :admin
  validate :something, on: :create
end

Khi bạn chạy user.valid?(:admin) hoặc user.save(context: admin), thì validate :something sẽ không hoặt động bởi vì chúng ta đã thay thế :create context thành :admin context

Vậy phải xử lý nó thế nào?

Chúng ta có thể kiểm tra lại cho cả hai context như sau:

class Admin::UsersController
  def edit
    User.transaction do
      @user = User.find(params[:id])
      if @user.valid?(:admin) && @user.valid?(:create)
        @user.save!(validate: false)
        redirect # ...
      else
        render # ...
      end
    end
  end
end

Và đây là 1 ví dụ có thể hữu ích cho bạn

class User < ActiveRecord::Base
  has_many :invoices
  validate :does_not_have_any_invoice, on: :destroy

  def destroy
    transaction do
      valid?(:destroy) or raise RecordInvalid.new(self)
      super()
    end
  end

  private

  def does_not_have_any_invoice
    errors.add(:invoices, :present) if invoices.exists?
  end
end

Ý tưởng là, nó không thể xóa người dùng đã tồn tại trong bảng invoices

0