急切加载协会的第一条记录

时间:2013-10-14 04:22:37

标签: ruby-on-rails

在一个由Rails应用程序制作的非常简单的论坛中,我在索引操作中从数据库中获取了30个主题,如下所示

def index

@topics = Topic.all.page(params[:page]).per_page(30)

end 

但是,当我在views / topics / index.html.erb中列出它们时,我还希望能够访问每个主题中的第一篇帖子以显示在工具提示中,这样当用户滚动时,他们可以阅读第一篇文章,无需点击链接。因此,在索引中每个帖子的链接中,我将以下内容添加到数据属性

topic.posts.first.body

每个链接都是这样的

<%= link_to simple_format(topic.name), posts_path(
:topic_id => topic), :data => { :toggle => 'tooltip', :placement => 'top', :'original-title' => "#{ topic.posts.first.body }"}, :class => 'tool' %>

虽然这种方法很好,但我担心这是一个n + 1查询,即如果有30个主题,它就会这样做30次

 User Load (0.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
  Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."topic_id" = $1 ORDER BY "posts"."id" ASC LIMIT 1  [["topic_id", 7]]

我注意到Rails会对其中的一些进行自动缓存,但我认为可能有一种方法可以不同地编写索引操作以避免一些n + 1问题,但我可以弄清楚如何。我发现我可以

include(:posts) 

急切加载帖子,就像这样

@topics = Topic.all.page(params[:page]).per_page(30).includes(:posts)

但是,如果我知道我只想要每个主题的第一篇帖子,有没有办法指定呢?如果一个主题有30个帖子,我不想急于加载所有这些帖子。

我试着做

.includes(:posts).first

但它破坏了代码

3 个答案:

答案 0 :(得分:1)

据我所知,你做不到。自定义关联通常用于允许除limit以外的包含条件。

  

如果您急切地使用指定的:limit选项加载关联,它将被忽略,返回所有关联的对象。 http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

class Picture < ActiveRecord::Base
  has_many :most_recent_comments, -> { order('id DESC').limit(10) }, 
                                 class_name: 'Comment'
end

Picture.includes(:most_recent_comments).first.most_recent_comments 
# => returns all associated comments.

答案 1 :(得分:1)

这似乎对我有用,所以请试一试,看看它是否适合你:

Topic.includes(:posts).where("posts.id = (select id from posts where posts.topic_id = topics.id limit 1)").references(:posts)

这将创建一个从属子查询,其中子查询中的posts topic_id与父查询中的topics id匹配。使用子查询中的limit 1子句,结果是每个Topic行只包含1个匹配的Post行,由于includes(:post)而急切加载。

请注意,在将SQL字符串传递给引用预先加载的关系的.where时,应附加references方法以通知ActiveRecord我们引用了一个关联,以便它知道在后续查询中执行适当的连接。显然它在没有这种方法的情况下在技术上工作,但是你得到了一个弃用警告,所以你不妨抛弃它,以免在将来的Rails更新中遇到问题。

答案 2 :(得分:0)

在尝试通过Rails“本机”解决此问题时,存在一些问题,这些问题在this question中进行了详细说明。

我们用SQL范围解决了它,对于您的情况,像这样:

class Topic < ApplicationRecord
  has_one :first_post, class_name: "Post", primary_key: :first_post_id, foreign_key: :id

  scope :with_first_post, lambda {
    select(
      "topics.*,
      (
        SELECT id as first_post_id
        FROM posts
        WHERE topic_id = topics.id
        ORDER BY id asc
        LIMIT 1
      )"
    )
  }
end

Topic.with_first_post.includes(:first_post)