简体   繁体   中英

Validating inclusion of a different model's descendants isn't working

I'm having some difficulty with a validation. I'm working with four models - Animal Cat Dog and Breed . Animal is an abstract class - yes, I realize Rails doesn't have those, but that's how I'm using it; it's never directly initialized, but a lot of common logic exists inside it for all its children to use. I'm also using STI, so Animal , Cat and Dog use the same table.

Breed meanwhile is going to be in a has_and_belongs_to_many relationship with Animal , but I want to restrict what breeds are available based on the model being used. So Breed has an attribute animal_type which is a string, that corresponds to the child class. The validation for this attribute is what I'm having trouble with. I'm trying to ensure you can only create a breed for a type of animal that exists.

Lastly, for organizational purposes, Animal and Breed are in an Animals module, to get them out of the way.

Here's how my code looks:

module Animals
    class Animals::Animal < ActiveRecord::Base
    end

    class Animals::Breed < ActiveRecord::Base
        validates :animal_type, inclusion: { in: Animal.descendants.map {|d| d.name}
    end
end

class Cat < Animals::Animal
end

class Dog < Animals::Animal
end

What should happen is the validator should be generating an array of the names of all child models of Animal , then it should compare the string animal_type and return true if the same value exists anywhere in the array.

I'm testing this with the console, and its failing despite me manually ensuring the values are equal. I'm not sure if it's caused by lazy loading (the first commands I run when I start the console are Cat.connection and Dog.connection so they appear in the Animal.descendants array), or by the module (since Animal and Breed are in the same module, I'm pretty sure I've got the reference correct).

I'm stuck, and I'm not an advanced enough user to know where to go next.

Looks like class loading will affect you, as long as the Animals module is all loaded in one go. At the point that the code inside the Breed class runs, there are no descendants of Animal yet, so the array passed to the validation is empty.

The inclusion validation allows you to specify the list with a lambda, ie

validates :animal_type, inclusion: { in: -> { Animal.descendants.map {|d| d.name}}

The lambda is called at the point where the validation is run, so you won't run into your current issue. You still need to ensure that the various descendant classes are loaded.

I poked around at your question for a bit. Best as I can tell, the inclusion validator doesn't like being passed the Animal.descendants.map {|d| d.name} Animal.descendants.map {|d| d.name} bit. So how about you just make a custom validation? Something like:

  module Animals
    class Animal < ActiveRecord::Base
    end

    class Breed < ActiveRecord::Base
      validate :animal_type_is_a_descendant_class

      private

      def animal_type_is_a_descendant_class
        if animal_type.nil?
          errors.add(:animal_type, "can't be blank")
        elsif !defined?(animal_type)
          errors.add(:animal_type, "is not a valid class")
        elsif !Animals::Animal.descendants.include?(animal_type.constantize)
          errors.add(:animal_type, "is not a descendant")
        end
      end

    end
  end

  class Cat < Animals::Animal
  end

  class Dog < Animals::Animal
  end

  class Horse
  end

If I load that into console and then do:

  b = Animals::Breed.new
  b.animal_type = "Cat"
  b.valid?

  b.animal_type = "Dog"
  b.valid?

  b.animal_type = nil
  b.valid?
  b.errors

  b.animal_type = "Horse"
  b.valid?
  b.errors

I get:

    irb(main):344:0* b = Animals::Breed.new
    => #<Animals::Breed:0x65639a0>
    irb(main):345:0> b.animal_type = "Cat"
    => "Cat"
    irb(main):346:0> b.valid?
    => true

    irb(main):347:0>
    irb(main):348:0* b.animal_type = "Dog"
    => "Dog"
    irb(main):349:0> b.valid?
    => true

    irb(main):350:0>
    irb(main):351:0* b.animal_type = nil
    => nil
    irb(main):352:0> b.valid?
    => false
    irb(main):353:0> b.errors
    => #<ActiveModel::Errors:0x60b9a30 @base=#<Animals::Breed:0x65639a0 @animal_type=nil, @validation_context=nil, @errors=#<ActiveModel::Errors:0x60b9a30 ...>>, @messages={:animal_type=>["can't be blank"]}>
    irb(main):354:0>

    irb(main):355:0* b.animal_type = "Horse"
    => "Horse"
    irb(main):356:0> b.valid?
    => false
    irb(main):357:0> b.errors
    => #<ActiveModel::Errors:0x60b9a30 @base=#<Animals::Breed:0x65639a0 @animal_type="Horse", @validation_context=nil, @errors=#<ActiveModel::Errors:0x60b9a30 ...>>, @messages={:animal_type=>["is not a descendant"]}>
    irb(main):358:0>

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