有很多直通 - 即使不存在关联也获取相关对象

has many through - get related object even if no associations exist

我已经建立了 has_many :through 关系:

class Dish
  has_many :dish_allergenes
  has_many :allergenes, :through => :dish_allergenes
end

class DishAllergene
  belongs_to :dish
  belongs_to :allergene
end


class Allergene
  has_many :dish_allergenes
  has_many :dishes, :through => :dish_allergenes
end

有大约 10 种过敏原的基本组合。添加或编辑一道菜时,应该可以直接调整过敏原。所以我的目标是将所有过敏原包含在一道菜中,即使不存在任何关联。

我知道我可以在表单中调用 Allergene.all,然后循环它们并检查 if dish.allergenes.include?(allergene.id),但这似乎是错误的。

我试过不同的连接,但它们只加载已经与菜肴相关的过敏原。 rails 方法是什么?

has_many :through 协会为此提供了一个工具 - 它是 allergen_ids 方法。从理论上讲,它只是一个包含所有 id 的数组,但它与 setter allergen_ids= 一起提供,它采用一个数组并在您的模型和具有给定 id 的模型之间创建关联。所以你需要做的就是在你的表单中添加以下内容:

<%= f.collection_check_boxes :allergen_ids, Allergen.all, :id, :name %>

Rails 将呈现所有过敏原的复选框,并使用提到的方法来决定要选择哪一个。之后它将以数组的形式发送所有这些,并将传递给提到的 setter.

现在,问题。与大多数属性编写器不同,将数组传递给 setter 实际上会调用数据库更改 - 在调用保存之前不会缓存它,并且无论验证是否失败都会执行。自然是不能接受的。我通常必须制定 "small" 解决方法(不要使用术语 "bloody hack")。技巧是覆盖 ids setter 和 getter 并在保存挂钩后创建。由于这是一个经常出现的问题,我通常将其放在一个单独的模块中:

module ActiveRecord::DirtyIds
  extend ActiveSupport::Concern

  module ClassMethods
    def has_dirty_ids_for(*associations)
      associations.each do |association|
        define_dirty_ids(association)
      end
    end

    private

    def define_dirty_ids(association)
      name = "#{association.to_s.singularize}_ids"

      # setter will store values in an instance variable
      define_method("#{name}_with_cache=") do |value|
        value = value.select(&:present?).map(&:to_i)
        attribute_will_change!(name) if send(name).sort != value.sort
        instance_variable_set(:"@#{name}", value)
      end

      # getter will read instance variable, if it is falsy fallback for default implementation
      define_method("#{name}_with_cache") do
        instance_variable_get(:"@#{name}") || send("#{name}_without_cache")
      end

      # override default association method so it reflects cached values
      define_method("#{association}_with_cache") do
        if instance_variable_get(:"@#{name}")
          association(association).klass.where(id: send("#{name}_with_cache"))
        else
          send("#{association}_without_cache")
        end
      end

      # after save hook calls the original method
      define_method("save_#{name}") do
        return if send(name) == send("#{name}_without_cache")
        send("#{name}_without_cache=", send(name))
      end

      private "save_#{name}"
      after_save "save_#{name}"

      alias_method_chain :"#{name}=", :cache
      alias_method_chain :"#{name}", :cache
      alias_method_chain :"#{association}", :cache
    end
  end
end

class ActiveRecord::Base
  include ActiveRecord::DirtyIds
end

将此代码放入初始化程序的新文件中。然后在您的模型中只需调用:

has_dirty_ids_for :allergenes

而且都应该是可缓存的。