【问题标题】:Association for polymorphic belongs_to of a particular type特定类型的多态属于_to 的关联
【发布时间】:2014-02-18 10:31:57
【问题描述】:

我对 Rails 比较陌生。我想为使用多态关联的模型添加关联,但只返回特定类型的模型,例如:

class Note < ActiveRecord::Base
  # The true polymorphic association
  belongs_to :subject, polymorphic: true

  # Same as subject but where subject_type is 'Volunteer'
  belongs_to :volunteer, source_association: :subject
  # Same as subject but where subject_type is 'Participation'
  belongs_to :participation, source_association: :subject
end

通过阅读 ApiDock 上的关联,我尝试了多种组合,但似乎没有什么能完全符合我的要求。这是我迄今为止最好的:

class Note < ActiveRecord::Base
  belongs_to :subject, polymorphic: true
  belongs_to :volunteer, class_name: "Volunteer", foreign_key: :subject_id, conditions: {notes: {subject_type: "Volunteer"}}
  belongs_to :participation, class_name: "Participation", foreign_key: :subject_id, conditions: {notes: {subject_type: "Participation"}}
end

我希望它通过这个测试:

describe Note do
  context 'on volunteer' do
    let!(:volunteer) { create(:volunteer) }
    let!(:note) { create(:note, subject: volunteer) }
    let!(:unrelated_note) { create(:note) }

    it 'narrows note scope to volunteer' do
      scoped = Note.scoped
      scoped = scoped.joins(:volunteer).where(volunteers: {id: volunteer.id})
      expect(scoped.count).to be 1
      expect(scoped.first.id).to eq note.id
    end

    it 'allows access to the volunteer' do
      expect(note.volunteer).to eq volunteer
    end

    it 'does not return participation' do
      expect(note.participation).to be_nil
    end

  end
end

第一个测试通过,但不能直接调用关系:

  1) Note on volunteer allows access to the volunteer
     Failure/Error: expect(note.reload.volunteer).to eq volunteer
     ActiveRecord::StatementInvalid:
       PG::Error: ERROR:  missing FROM-clause entry for table "notes"
       LINE 1: ...."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."s...
                                                                    ^
       : SELECT  "volunteers".* FROM "volunteers"  WHERE "volunteers"."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."subject_type" = 'Volunteer' LIMIT 1
     # ./spec/models/note_spec.rb:10:in `block (3 levels) in <top (required)>'

为什么?

我想这样做的原因是因为我正在构建一个基于解析查询字符串的范围,包括加入各种模型/等;用于构造范围的代码比上面的代码复杂得多——它使用collection.reflections 等。我目前的解决方案适用于此,但它冒犯了我,我不能直接从 Note 实例调用关系。

我可以通过将其拆分为两个问题来解决它:直接使用作用域

  scope :scoped_by_volunteer_id, lambda { |volunteer_id| where({subject_type: 'Volunteer', subject_id: volunteer_id}) }
  scope :scoped_by_participation_id, lambda { |participation_id| where({subject_type: 'Participation', subject_id: participation_id}) }

然后只对note.volunteer/note.participation 使用getter,如果它有正确的subject_type,则返回note.subject(否则为零),但我认为在Rails 中一定有更好的方法?

【问题讨论】:

  • 有点神秘的{notes: {subject_type: "Volunteer"}} 子句有notes:,否则它会查询不存在的volunteers.subject_type 列,这使它查询notes.subjects_type。我已经确定了等效的{'notes.subject_type': "Volunteer"} 语法;它还提醒我 notes 应该是复数表名,而不是(有时是单数)关联名称...

标签: ruby-on-rails ruby-on-rails-3 polymorphic-associations


【解决方案1】:

我遇到了类似的问题。最后,我通过使用如下所示的自引用关联,找到了最好、最强大的解决方案。

class Note < ActiveRecord::Base
  # The true polymorphic association
  belongs_to :subject, polymorphic: true

  # The trick to solve this problem
  has_one :self_ref, :class_name => self, :foreign_key => :id

  has_one :volunteer, :through => :self_ref, :source => :subject, :source_type => Volunteer
  has_one :participation, :through => :self_ref, :source => :subject, :source_type => Participation
end

简洁明了,仅在 Rails 4.1 上测试过,但我想它应该适用于以前的版本。

【讨论】:

  • 这很好——感谢分享!有点奇怪,这是必要的,真的……
  • 与问题无关,但与此答案相关:我认为建议不要在关联定义中使用常量。而不是VolunteerParticipation,您需要使用"Volunteer":Volunteer"Participation":Participation。在这里讨论:github.com/rails/rails/pull/8357
  • 没有self_ref:belongs_to:志愿者,foreign_key::subject_id,foreign_type::subject_type,class_name:志愿者,多态:true
  • 分配不能以这种方式正常工作。如果您执行Note.create(volunteer: Volunteer.create),则subject_idsubject_type 将为nil,尽管您仍然可以从read 方法中检索志愿者。
  • @stackNG,这是一个很好的解决方案,可能是我在这个问题上找到的最干净的解决方案。我的一个问题是它添加了一个不必要的额外连接。我希望这不是必需的,我正在下面发布一个替代解决方案,但这并不简单,但确实消除了额外的查询。
【解决方案2】:

我找到了一种解决此问题的骇人听闻的方法。我的一个项目中有一个类似的用例,我发现它可以工作。在您的 Note 模型中,您可以添加如下关联:

class Note
  belongs_to :volunteer, 
    ->(note) {where('1 = ?', (note.subject_type == 'Volunteer')},
    :foreign_key => 'subject_id'
end

您需要为要附加注释的每个模型添加其中一个。为了使这个过程更干燥,我建议创建一个像这样的模块:

 module Notable
   def self.included(other)
     Note.belongs_to(other.to_s.underscore.to_sym, 
       ->(note) {where('1 = ?', note.subject_type == other.to_s)},
       {:foreign_key => :subject_id})
   end
 end

然后将其包含在您的志愿者和参与模型中。

[编辑]

更好的 lambda 是:

 ->(note) {(note.subject_type == "Volunteer") ? where('1 = 1') : none}

由于某种原因,将“where”替换为“all”似乎不起作用。另请注意,“无”仅在 Rails 4 中可用。

[摩尔编辑]

我没有运行 rails 3.2 atm,所以我无法测试,但我认为您可以通过使用 Proc 来获得类似的结果,例如:

belongs_to :volunteer, :foreign_key => :subject_id, 
  :conditions => Proc.new {['1 = ?', (subject_type == 'Volunteer')]}

可能值得一试

【讨论】:

  • 有趣的hack,遗憾的是Rails 3.2 似乎不允许将范围作为belongs_to 关系的一部分。 :((升级到 Rails 4 在我的待办事项清单上,但还有很多其他事情要做……)不过好主意!
  • @Benjie 使用替代方法编辑答案(可能在 3.2 中有效)
  • 感谢您的新尝试 - 抱歉,我花了这么长时间才尝试。遗憾的是它似乎不起作用(undefined method subject_type),我认为这是因为: > 在 proc 中,self 是关联所有者的对象,除非您急于加载关联,在这种情况下 self 是关联所在的类。 -- guides.rubyonrails.org/3_1_release_notes.html
【解决方案3】:

我被这种反向关联困住了,在Rails 4.2.1 我终于发现了这一点。希望这对使用较新版本的 Rails 的人有所帮助。关于我遇到的问题,您的问题与我发现的最接近。

belongs_to :volunteer, foreign_key: :subject_id, foreign_type: 'Volunteer'

【讨论】:

  • 对我来说,这种方法“运行”,但joins 产生的 SQL 实际上并没有限制subject_type [Rails 5.0]
  • @BeniCherniavsky-Paskin 我偶然发现了同样的问题,如果有另一个具有相同 id 的模型,它只会在类型不匹配时返回此事件
  • 为了解决subject_type没有加入join的问题,我做了这个变通方法:belongs_to :volunteer, -&gt; { where(notes: { subject_type: :Volunteer }) }, foreign_key: :subject_id, foreign_type: :Volunteer, optional: true希望有用
  • 不再适用于 rails 6.1。 foreign_type 不允许使用。
  • @DmitryPolushkin 在 rails 6.x 中是否有等效的方法来完成?
【解决方案4】:

你可以这样做:

belongs_to :volunteer, -> {
  where(notes: { subject_type: 'Volunteer' }).includes(:notes)
}, foreign_key: :subject_id

includesleft join 对应,因此您与所有volunteerparticipation 具有notes 关系。然后你通过subject_id 找到你的记录。

【讨论】:

  • 你不需要optional: true 吗?
  • 我认为您可以使用joins 代替includes - 这应该足够了。
  • @Matt 我用joins 尝试过(因为这当然是有道理的),但是joins 不能完全正确地工作,因为它可能导致返回重复的行。之后我也尝试添加distinct,但这仍然不起作用。我不知道细节,但includes 工作正常,没有返回重复记录。
  • 当笔记的主题是不同类型的对象,但与现有志愿者具有相同的 ID 时,这不起作用。例如。对于具有subject_type: "Participation", subject_id: 1 属性的注释(称为a_note),并且假设数据库包含id: 1 的参与和志愿者记录,那么a_note.volunteer 将返回志愿者记录,而不是nil,即使a_note.subject_type"Participation"。如果我找到解决办法,会在这里更新
【解决方案5】:

我相信我已经找到了一种体面的方法来处理这个问题,它也涵盖了人们可能需要的大多数用例。

我会说,这是一个很难找到答案的问题,因为很难弄清楚如何提出这个问题,也很难清除所有只是标准 Rails 答案的文章。我认为这个问题属于高级 ActiveRecord 领域。

本质上,我们要做的是向模型添加关系,并且仅在建立关联的模型满足某些先决条件时才使用该关联。例如,如果我有 SomeModel 类,并且它有称为“some_association”的 belongs_to 关联,我们可能希望在 SomeModel 记录上应用一些必须为真的先决条件,这些条件会影响 :some_association 是否返回结果。在多态关系的情况下,前提条件是多态类型列是一个特定的值,如果不是那个值,它应该返回nil。

解决这个问题的难度因不同的方法而复杂化。我知道三种不同的访问模式:对实例的直接访问(例如:SomeModel.first.some_association)、:joins(例如:SomeModel.joins(:some_association) 和 :includes(例如:SomeModel.includes(:some_association) )(注意:eager_load 只是连接的一种变体)。每种情况都需要以特定方式处理。

今天,由于我基本上一直在重新审视这个问题,我想出了以下实用方法,它充当了 belongs_to 的一种包装方法。我猜类似的方法可以用于其他关联类型。

  # WARNING: the joiner table must not be aliased to something else in the query,
  #  A parent / child relationship on the same table probably would not work here
  # TODO: figure out how to support a second argument scope being passed
  def self.belongs_to_with_prerequisites(name, prerequisites: {}, **options)
    base_class = self
    belongs_to name, -> (object=nil) {
      # For the following explanation, assume we have an ActiveRecord class "SomeModel" that has a belongs_to
      #   relationship on it called "some_association"
      # Object will be one of the following:
      #   * nil - when this association is loaded via an :includes.
      #     For example, SomeModel.includes(:some_association)
      #   * an model instance - when this association is called directly on the referring model
      #     For example: SomeModel.first.some_association, object will equal SomeModel.first
      #   * A JoinDependency - when we are joining this association
      #     For example, SomeModel.joins(:some_assocation)
      if !object.is_a?(base_class)
        where(base_class.table_name => prerequisites)
      elsif prerequisites.all? {|name, value| object.send(name) == value}
        self
      else
        none
      end
    },
    options
  end

该方法需要注入到 ActiveRecord::Base 中。

那么我们可以像这样使用它:

  belongs_to_with_prerequisites :volunteer,
    prerequisites: { subject_type: 'Volunteer' },
    polymorphic: true,
    foreign_type: :subject_type,
    foreign_key: :subject_id

它可以让我们做以下事情:

Note.first.volunteer
Note.joins(:volunteer)
Note.eager_load(:volunteer)

但是,如果我们尝试这样做,我们会得到一个错误:

Note.includes(:volunteer)

如果我们运行最后一段代码,它会告诉我们在志愿者表中不存在 subject_type 列。

所以我们必须在 Notes 类中添加一个作用域,并按如下方式使用:

class Note < ActiveRecord::Base
  belongs_to_with_prerequisites :volunteer,
        prerequisites: { subject_type: 'Volunteer' },
        polymorphic: true,
        foreign_type: :subject_type,
        foreign_key: :subject_id
  scope :with_volunteer, -> { includes(:volunteer).references(:volunteer) }
end
Note.with_volunteer

因此,归根结底,我们没有 @stackNG 的解决方案所具有的额外连接表,但该解决方案绝对更有说服力且不那么老套。我想我还是会发布这个,因为它是一个非常彻底的调查的结果,可能会帮助其他人了解这些东西是如何工作的。

【讨论】:

    猜你喜欢
    • 2019-09-23
    • 1970-01-01
    • 2020-07-08
    • 1970-01-01
    • 1970-01-01
    • 2011-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多