ruby-on-rails 特定类型的多态belongs_to的关联

cidc1ykv  于 2022-12-15  发布在  Ruby
关注(0)|答案(6)|浏览(207)

我对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(否则为nil),但我认为在Rails中一定有更好的方法?

ekqde3dh

ekqde3dh1#

我也遇到过类似的问题,最后我使用了如下的自参考关联,找到了最好最健壮的解决方案。

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

干净简单,只在Rails4.1上测试过,但我想它应该可以在以前的版本中使用。

fslejnso

fslejnso2#

我发现了一个解决这个问题的方法,我在一个项目中有一个类似的用例,我发现这个方法很有效,在你的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}

由于某种原因,用“all”替换“where”似乎不起作用。还要注意,“none”只在Rails4中可用。
[MOAR编辑]
我没有运行rails 3.2 atm,所以无法进行测试,但我认为您可以通过使用Proc来获得类似的结果,例如:

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

也许值得一试

3pmvbmvn

3pmvbmvn3#

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

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

gab6jxml4#

你可以这样做:

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

includes做了left join,所以你有了notes,与所有volunteerparticipation的关系,然后你通过subject_id找到你的记录。

myss37ts

myss37ts5#

我相信我已经找到了一个体面的方式来处理这一点,也涵盖了大多数用例,一个人可能需要。
我会说,这是一个很难找到答案的问题,因为很难弄清楚如何提出这个问题,也很难剔除所有只是标准Rails答案的文章。我认为这个问题福尔斯高级ActiveRecord领域。
本质上,我们尝试做的是将关系添加到模型中,并且只有在创建关联的模型满足某些先决条件时才使用该关联。例如,如果我有类SomeModel,并且它具有名为“some_association”的belongs_to关联,我们可能希望在SomeModel记录上应用某些先决条件,这些条件必须为真,以影响some_association是否返回结果。对于多态关系,先决条件是多态类型列是特定值,如果不是该值,则应返回nil。
解决这个问题的困难,因途径不同而更形复杂,我知道有三种不同的途径:示例上的直接访问(例如:某个模型.第一个.某个关联),:连接(例如:连接(:some_association)和:包含(例如:某个模型。包括(:某个关联))(注意:eager_load只是连接的一个变体)。这些情况中的每一种都需要以特定的方式处理。
今天,由于我基本上重新讨论了这个问题,我提出了以下实用方法,它充当了belongs_to的一种 Package 方法。我猜类似的方法也可以用于其他关联类型。

# 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的解决方案所拥有的额外的连接表,但是这个解决方案肯定更有说服力,也不那么笨拙。我想我还是要发布这个,因为它是一个非常彻底的调查的结果,可能会帮助其他人理解这个东西是如何工作的。

ltqd579y

ltqd579y6#

这里有一个使用参数化作用域的不同选项,根据外部类型,作用域将被设置为allnone,以便关系在类型正确时返回相关模型,否则返回nil

class Note < ActiveRecord::Base
  belongs_to :subject, polymorphic: true

  belongs_to :volunteer, 
    -> (note) { note.subject_type == "Volunteer" ? all : none },
    foreign_key: :subject_id
  
  belongs_to :participation,
    -> (note) { note.subject_type == "Participation" ? all : none },
    foreign_key: :subject_id
end

相关问题