关联特定类型的多态belongs_to

时间:2014-02-18 10:31:57

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

我对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只返回note.subject,如果它有正确的subject_type(否则为nil),但我想在Rails中必须是一个更好的方式?

5 个答案:

答案 0 :(得分:26)

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

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

清洁&amp;简单,只在Rails 4.1上测试过,但我想它应该适用于以前的版本。

答案 1 :(得分:6)

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

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

答案 2 :(得分:2)

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

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

您需要为要添加备注的每个模型添加其中一个。为了使这个过程DRYer我建议创建一个这样的模块:

 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}

由于某种原因取代&#39;其中&#39;与所有&#39;似乎不起作用。另请注意,“没有”&#39;仅适用于Rails 4。

[MOAR EDIT]

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

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

可能值得一试

答案 3 :(得分:1)

你可以这样做:

includes

left join执行notes,因此您与所有volunteerparticipation保持subject_id关系。然后你通过 protected void AddItem(object sender, EventArgs e) { string insertCmd = "INSERT INTO Picture (Album, id) VALUES (@Album, @id)"; using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["strConn"].ConnectionString)) { conn.Open(); SqlCommand myCommand = new SqlCommand(insertCmd, conn); // Create parameters for the SqlCommand object // initialize with input-form field values myCommand.Parameters.AddWithValue("@Album", txtAlbum.Text); myCommand.Parameters.Add("@id", SqlDbType.Int).Direction = ParameterDirection.Output; myCommand.ExecuteNonQuery(); int id = (int)myCommand.Parameters["@id"].Value; } } 找到你的记录。

答案 4 :(得分:0)

我相信我已经找到了解决这个问题的一种不错的方法,该方法还涵盖了人们可能需要的大多数用例。

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

基本上,我们想要做的是向模型添加关系,并且仅在建立关联的模型上满足某些先决条件时才使用该关联。例如,如果我有SomeModel类,并且它具有名为“ some_association”的belongs_to关联,则我们可能想在SomeModel记录上应用某些必须为true的先决条件,这些条件会影响: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解决方案所拥有的额外联接表,但是该解决方案绝对更雄辩,更不容易破解。我想无论如何都会发布此内容,因为它是经过非常彻底的调查的结果,可能会帮助其他人了解这些内容的工作原理。