简体   繁体   中英

Rails model attribute is nil in custom validator method

I have a polymorphic table, and I want to be able to verify that the entity id provided on create does in fact reference an existing object. I wrote a custom validator to do just this, which is invoked when I call find_or_create_by . However, the attributes that I am performing validation on are nil in the validation method.

  validate :validate_id, on: :create
...

  def validate_id
    klass = Object.const_get(entity_type)

    return if klass.exists?(entity_id)

    errors.add(:entity_id, 'is invalid')
  end

entity_type and entity_id are both model members, and are passed as args to find_or_create_by . I don't understand why they are nil in the validation method.

The reason those values don't hold anything in your method is because they are only scoped to that method, and that method does not take them in as arguments. The best way to accomplish what you're trying to do is to write a proper custom validator that inherits from ActiveModel::EachValidator and implements the validate_each(record, attribute, value) method.

Although you can get away with a validates_each block:

validates_each :entity_id do |record, attr, value|
  return if Entity.exists?(value)
  errors.add(attr, 'is invalid')
end

However...

Why not just use a normal inclusion validator?
Since I'm on Rails 4.2 right now this is how I do it when Entity record counts are low enough for it not to cause performance issues.
(I'd use a better error message, but using yours for simplicity.)

  validates :entity_id,
            inclusion: { in:      proc { Entity.all.pluck(:id) },
                         message: 'is invalid' }

Edit: I see now the issue is that your polymorphic entity means the examples above won't work without some tweaking since Entity could be either of two models EntityOne or EntityTwo . You could easily check if either of them contain the ID in question like this:

validates_each :entity_id do |record, attr, value|
  return if EntityOne.exists?(value) || EntityTwo.exists?(value)
  errors.add(attr, 'is invalid')
end

Or

  validates :entity_id,
            inclusion: { in:      proc { EntityOne.all.pluck(:id) + EntityTwo.all.pluck(:id) },
                         message: 'is invalid' }

But this isn't a great validation since there are invalid situations passed through as valid.

You might have to resort to a proper custom validator that inherits from ActiveModel::Validator and implements the validate(record) method which also accepts options where you can pass in the type of entity expected. It would look like this:

# Inside the model:
validates_with(EntityIdValidator, expected_entity: entity_type)



#The custom validator:
class EntityIdValidator < ActiveModel::Validator

  def validate(record)
    expected_entity = options[:expected_entity]
    return if expected_entity.exists?(record.entity_id)

    record.errors.add(:entity_id, 'is invalid')
  end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM