关联特定类型的多态belongs_to

Ben*_*jie 19 ruby-on-rails polymorphic-associations ruby-on-rails-3

我对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
Run Code Online (Sandbox Code Playgroud)

我通过阅读有关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
Run Code Online (Sandbox Code Playgroud)

我希望它通过这个测试:

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
Run Code Online (Sandbox Code Playgroud)

第一个测试通过,但您无法直接调用该关系:

  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)>'
Run Code Online (Sandbox Code Playgroud)

为什么?

我想这样做的原因是因为我正在构建一个基于解析查询字符串的范围,包括加入各种模型/等; 用于构造范围的代码比上面的代码复杂得多 - 它使用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}) }
Run Code Online (Sandbox Code Playgroud)

然后只是使用一个getter来获取note.volunteer/ note.participation只是返回note.subject它是否正确subject_type(否则为nil)但是我想在Rails中必须有更好的方法吗?

小智 31

我碰到了类似的问题.我终于通过使用如下的自引用关联来解决最好和最强大的解决方案.

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
Run Code Online (Sandbox Code Playgroud)

干净简单,只在Rails 4.1上测试过,但我想它应该适用于以前的版本.

  • 如果没有self_ref:属于:自愿者,Foreign_key::主题ID,foreign_type::subject_type,类别名称:志愿者,多态:true (3认同)
  • 与问题无关,但与此答案相关:我认为建议不要在关联定义中使用常量。您需要使用“Volunteer”或“:Volunteer”和“Participation”或“:Participation”,而不是“Volunteer”和“Participation”。此处讨论:https://github.com/rails/rails/pull/8357 (2认同)
  • 这样分配就无法正常工作。如果你执行`Note.create(volunteer: Volunteer.create)`,那么`subject_id`和`subject_type`将为nil,尽管你仍然可以从read方法中检索志愿者。 (2认同)
  • `class_name: self.name` 也有效。 (2认同)

Sli*_*pan 6

我找到了一种解决这个问题的黑客方法。我的一个项目中有一个类似的用例,我发现这是可行的。在您的 Note 模型中,您可以添加如下关联:

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

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

 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
Run Code Online (Sandbox Code Playgroud)

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

[编辑]

稍微好一点的 lambda 是:

 ->(note) {(note.subject_type == "Volunteer") ? where('1 = 1') : none}
Run Code Online (Sandbox Code Playgroud)

由于某种原因,用“全部”替换“哪里”似乎不起作用。另请注意,“none”仅在 Rails 4 中可用。

[莫尔编辑]

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

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

可能值得一试


twm*_*loy 6

我被困在这种反向关联中,Rails 4.2.1我终于发现了这一点.希望这可以帮助某人,如果他们使用更新版本的Rails.你的问题与我发现的问题最接近.

belongs_to :volunteer, foreign_key: :subject_id, foreign_type: 'Volunteer'
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,这种方法"运行",但是`joins`产生的SQL实际上并没有约束`subject_type` [Rails 5.0] (7认同)
  • @BeniCherniavsky-Paskin 我偶然发现了同样的问题,如果有另一个具有相同 ID 的模型,它只会在类型不匹配时返回此事件 (3认同)
  • 为了解决 join 中未包含 `subject_type` 的问题,我做了以下解决方法: `belongs_to :volunteer, -&gt; { where(notes: { subject_type: :Volunteer }) },foreign_key: :subject_id,foreign_type: :Volunteer,可选:true`我希望它可能有用 (2认同)
  • 不再适用于 Rails 6.1。不允许使用“foreign_type”。 (2认同)