【问题标题】:Rails: includes with polymorphic associationRails:包含多态关联
【发布时间】:2014-02-25 11:27:35
【问题描述】:

我读到了这篇关于Using Polymorphism to Make a Better Activity Feed in Rails 的有趣文章。

我们最终会得到类似的东西

class Activity < ActiveRecord::Base
  belongs_to :subject, polymorphic: true
end

现在,如果其中两个主题是:

class Event < ActiveRecord::Base
  has_many :guests
  after_create :create_activities
  has_one :activity, as: :subject, dependent: :destroy 
end

class Image < ActiveRecord::Base
  has_many :tags
  after_create :create_activities
  has_one :activity, as: :subject, dependent: :destroy 
end

将 create_activities 定义为

def create_activities
  Activity.create(subject: self)
end

客人和标签定义为:

class Guest < ActiveRecord::Base
  belongs_to :event
end

class Tag < ActiveRecord::Base
  belongs_to :image
end

如果我们查询最近记录的 20 个活动,我们可以这样做:

Activity.order(created_at: :desc).limit(20)

我们有第一个可以解决的 N+1 查询问题:

Activity.includes(:subject).order(created_at: :desc).limit(20)

但是,当我们调用客人或标签时,我们又遇到了一个N​​+1查询问题。

为了能够使用分页,解决这个问题的正确方法是什么?

【问题讨论】:

  • gueststags 是什么?请提供相关代码
  • @xlembouras 完成了,但我很确定你已经猜到它们是什么了
  • 是的,我看不到 EventImageGuestTag 是如何连接到 Activity
  • @xlembouras 完成并删除用户以简化

标签: ruby-on-rails performance activerecord polymorphic-associations


【解决方案1】:

编辑 2:我现在使用的是 rails 4.2,并且急切加载多态性现在是一项功能 :)

编辑:这似乎在控制台中工作,但由于某种原因,我建议使用下面的部分仍然会生成带有子弹 gem 的 N+1 查询堆栈警告。我需要调查...

好的,我找到了解决方案([编辑] 还是我?),但它假定您知道所有主题类型。

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

  belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id
  belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id
end

现在你可以做

Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)

但要使急切加载工作,您必须使用例如

activity.event.guests.first

而不是

activity.part.guests.first

所以你可能可以定义一个方法来代替主题

def eager_loaded_subject
  public_send(subject.class.to_s.underscore)
end

所以现在你可以查看

render partial: :subject, collection: activity

部分与

# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject

还有两个(虚拟)部分

# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>

# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>

【讨论】:

  • 太棒了!谢谢你。
  • 只是为了强调一点——您需要使用您创建的自定义 belongs_to 范围(此处为 activity.event ...)来访问嵌套资源。如果你对为什么 Rails 似乎预加载了你的关联但仍然喜欢 N+1 感到困惑,这肯定是原因。
  • 我缺少包含在 :event 和 :tag 关联中的内容。谢谢!您应该将其标记为正确答案。
  • @Tashows 很高兴它帮助了你!它太老了,我不记得为什么我接受了另一个答案,所以我会相信我以前的自己,这是有充分理由的^^
【解决方案2】:

这有望在 rails 5.0 中得到修复。已经有一个问题和一个拉取请求。

https://github.com/rails/rails/pull/17479

https://github.com/rails/rails/issues/8005

我已经分叉了导轨并将补丁应用于 4.2-stable,它对我有用。尽管我不能保证定期与上游同步,但请随意使用我的 fork。

https://github.com/ttosch/rails/tree/4-2-stable

【讨论】:

  • 相关的拉取请求仍处于打开状态。所以它不会在 Rails 5.0 或 5.1 中修复。
  • github.com/rails/rails/pull/32655#issuecomment-383227906 是 2018 年 4 月 21 日合并到 rails master 的最终 PR。但是在底部 5-2-stable 的更改日志中提到了,所以我认为该功能应该在 Rails 5.2 中可用.x 及更高版本。
  • 除非我遗漏了一些没有进入 5.2.3 的东西。
【解决方案3】:

您可以使用ActiveRecord::Associations::Preloader 预加载分别链接到每个事件和图像对象的客人和标签,这些对象和图像对象作为主题与活动集合相关联。

class ActivitiesController < ApplicationController
  def index
    activities = current_user.activities.page(:page)

    @activities = Activities::PreloadForIndex.new(activities).run
  end
end

class Activities::PreloadForIndex
  def initialize(activities)
    @activities = activities
  end

  def run
    preload_for event(activities), subject: :guests
    preload_for image(activities), subject: :tags
    activities
  end

  private

  def preload_for(activities, associations)
    ActiveRecord::Associations::Preloader.new.preload(activities, associations)
  end

  def event(activities)
    activities.select &:event?
  end

  def image(activities)
    activities.select &:image?
  end
end

【讨论】:

  • 您的链接不再可用:-(
  • @SvenR。显然那个网站已经不存在了。由于答案包含完整信息,因此不需要该链接。所以我把它删了。
【解决方案4】:
image_activities = Activity.where(:subject_type => 'Image').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => 'Event').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)

【讨论】:

    【解决方案5】:

    我建议将多态关联添加到您的 EventGuest 模型中。 polymorphic doc

    class Event < ActiveRecord::Base
      has_many :guests
      has_many :subjects
      after_create :create_activities
    end
    
    class Image < ActiveRecord::Base
      has_many :tags
      has_many :subjects
      after_create :create_activities
    end
    

    然后尝试做

    Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
    

    【讨论】:

    • 我再次编辑了问题。 Event 和 Image 只有一个活动,在创建时创建。您写的内容将无法通过,您正在调用事件。甚至(主题::guests)也不起作用(相反,它会因图像而中断)。
    【解决方案6】:

    这会生成有效的 SQL 查询,还是因为 events 不能与 tagsimagesJOINed 和 guests 一起导致 events 失败?

    class Activity < ActiveRecord::Base
      self.per_page = 10
    
      def self.feed
        includes(subject: [:guests, :tags]).order(created_at: :desc)
      end
    end
    
    # in the controller
    
    Activity.feed.paginate(page: params[:page])
    

    这将使用will_paginate

    【讨论】:

    • 这也是我的猜测,但它不起作用。如果第一个结果是一个事件,你会得到一个错误“ActiveRecord::ConfigurationError: Association named 'tags' was not found on Event”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-06
    • 2021-03-23
    • 2019-03-14
    • 2016-04-05
    • 1970-01-01
    相关资源
    最近更新 更多