如何在Rails中替换内存中的模型关联?

时间:2017-02-03 00:56:02

标签: ruby-on-rails activerecord rails-activerecord

假设我有一个人物模型,一个人有很多宠物,一只宠物与狗和猫有多态关系。请注意,一个人不能拥有他/她过敏的动物。

class Person < ActiveRecord::Base
  has_many :pets, dependent: :destroy
  validate :not_allergic_to_animal

  def not_allergic_to_animal
    if allergic_to_cats? && owns_a_cat?
      errors.add(:pets, 'cannot have a cat if allergic to cats')
    elsif allergic_to_dogs? && owns_a_dog?
      errors.add(:pets, 'cannot have a dog if allergic to dogs')
    end
  end

  def owns_a_cat?
    pets.any? { |pet| pet.animal_type == Cat.name }
  end

  def owns_a_dog?
    pets.any? { |pet| pet.animal_type == Dog.name }
  end
end

class Pet < ActiveRecord::Base
  belongs_to :user
  belongs_to :animal, polymorphic: true

  validates :animal_type, inclusion: { in: [Cat.name, Dog.name] } 
end

class Cat < ActiveRecord::Base
end

class Dog < ActiveRecord::Base
end

现在让我们说人鲍勃已经有2只猫,一只名叫爱丽丝,另一只名叫阿黛尔。

bob = Person.where(name: 'Bob')

alice = Cat.where(name: 'Alice')
bob.pets.create!(animal: alice)

adele = Cat.where(name: 'Adele')
bob.pets.create!(animal: adele)
鲍勃喜欢爱丽丝,但阿黛尔是一只非常激进的猫,所以鲍勃不再想要了。他还想要一只狗。因此,他决定向他的终端做一个PUT请求。 3是杰克狗的身份,1是爱丽丝猫的身份。

PUT /me/ {
  "pets": {
    "dog_ids": [3],
    "cat_ids": [1]
  }
}

现在问题!

如何在内存中替换Bob的宠物,验证Bob确定他对任何宠物都不过敏,然后保存关联(不使用数据库事务)?

我知道可以使用#build在内存中创建关联,但我不想附加到现有关联,但替换现有关联,所有在内存中< / strong>保存之前。因此,上述示例将在验证Person实例后进行2次数据库查询,一次用于为Jack狗创建Pet关联,另一次用于删除Adele对猫的关联,验证之后。

我尝试操作ActiveRecord::Associations::CollectionProxy,但在删除或添加元素时,它总是在数据库中提交。

请注意,我不想创建一个param验证器,只是验证id,其他接口可用于更新Person,验证需要在所有这些接口上保持一致并靠近模型。

修改

我也尝试将#build#mark_for_destruction结合使用,但是替换将无效,现有的关联将在保存后重新创建。

2 个答案:

答案 0 :(得分:1)

我刚刚带来的解决方案:

标记破坏的宠物animal_id不在提供的ID中,构建不存在的动物,并跳过在验证中标记为销毁的动物。

class Person < ActiveRecord::Base
  has_many :pets, dependent: :destroy
  validate :not_allergic_to_animal

  def not_allergic_to_animal
    if allergic_to_cats? && owns_a_cat?
      errors.add(:pets, 'cannot have a cat if allergic to cats')
    elsif allergic_to_dogs? && owns_a_dog?
      errors.add(:pets, 'cannot have a dog if allergic to dogs')
    end
  end
  def owns_a_cat?
    pets
      .reject(&:marked_for_destruction?)
      .any? { |pet| pet.animal_type == Cat.name }
  end

  def owns_a_dog?
    pets
      .reject(&:marked_for_destruction?)
      .any? { |pet| pet.animal_type == Dog.name }
  end
end


bob = Person.where(name: 'Bob')

alice = Cat.where(name: 'Alice')
bob.pets.create!(animal: alice)

adele = Cat.where(name: 'Adele')
bob.pets.create!(animal: adele)

jack = Dog.create

bob.pets.find_by(animal_type: Cat.name, animal_id: adele.id).mark_for_destruction

bob.pets.build(animal: jack)
bob.valid?
bob.save!

答案 1 :(得分:0)

您可以使用pets=方法直接指定集合。例如,

# Find the first pet
p1 = bob.pets.find_by_animal_id(alice.id)

# Create the second pet, this won't save yet
jack = Dog.find(3)
p2 = Pet.new(animal: jack)

bob.pets = [p1, p2]