如何在Rails中使用:include避免多次查询?

14
如果我这样做
post = Post.find_by_id(post_id, :include => :comments)

进行了两个查询(一个用于帖子数据,另一个用于帖子的评论)。然后当我执行post.comments时,不会再执行另一个查询,因为数据已经被缓存。

是否有一种方法可以只执行一个查询,同时仍然通过post.comments访问评论?


似乎不可能。我尝试在评论上使用外连接。这确实只执行了一个查询,但结果没有被缓存。因此,当我调用post.comments时,它会执行另一个查询。如果有人能想出答案,我会很感兴趣。 - Mischa
2个回答

37

不,没有。这是:include的预期行为,因为JOIN方法最终会变得低效。

例如,考虑以下情况:需要选择Post模型的3个字段,2个字段用于Comment,并且这篇文章有100条评论。Rails可以运行单个JOIN查询,如下所示:

SELECT post.id, post.title, post.author_id, comment.id, comment.body
FROM posts
INNER JOIN comments ON comment.post_id = post.id
WHERE post.id = 1

这会返回以下结果表:

 post.id | post.title | post.author_id | comment.id | comment.body
---------+------------+----------------+------------+--------------
       1 | Hello!     |              1 |          1 | First!
       1 | Hello!     |              1 |          2 | Second!
       1 | Hello!     |              1 |          3 | Third!
       1 | Hello!     |              1 |          4 | Fourth!
...96 more...
你已经看到了问题。虽然单查询的JOIN方法返回你需要的数据,但是它会冗余地返回多次。当数据库服务器将结果集发送给Rails时,每个帖子的ID、标题和作者ID都会被发送100次。现在,假设你对Post感兴趣的有10个字段,其中8个是文本块。噫。那就是很多数据。从数据库传输数据到Rails确实需要CPU周期和RAM来处理,所以尽量减少数据传输对于使应用程序运行更快、更轻巧非常重要。
Rails开发人员计算过,大多数应用程序在使用多个查询只获取每个数据一次而不是可能产生巨大冗余的单个查询时运行得更好。
当然,每个开发者的生涯中总会有一个时刻,需要使用:joins来替换:include才能运行复杂的条件语句,但对于预取关系,Rails采用的:include方法对性能更好。

嘿Matchu,看起来使用:joins方法比使用:include方法更有效率,尤其是在has_one关系中。这是真的吗?还是我应该继续使用:include方法? - rayban
@rayban:有趣。我猜那应该能很好地工作,但我不确定 :joins 是否正确实例化其他对象,或者它是否最终会为每个对象触发另一个查询(尽管它真的不应该这样)。也许值得运行自己的基准测试来找出答案 :/ - Matchu

5
如果您使用预加载关联的这种行为,您将获得单个(且高效)查询。
以下是一个示例:
  • Say you have the following model (where :user is the foreign reference):

    class Item < ActiveRecord::Base
      attr_accessible :name, :user_id
      belongs_to :user
    end
    
  • Then executing this (note: the where part is crucial as it tricks Rails to produce that single query):

    @items = Item.includes(:user).where("users.id IS NOT NULL").all
    

    will result in a single SQL query (the syntax below is that of PostgreSQL):

    SELECT "items"."id" AS t0_r0, "items"."user_id" AS t0_r1, 
            "items"."name" AS t0_r2, "items"."created_at" AS t0_r3,
            "items"."updated_at" AS t0_r4, "users"."id" AS t1_r0, 
            "users"."email" AS t1_r1, "users"."created_at" AS t1_r4, 
            "users"."updated_at" AS t1_r5 
    FROM "measurements" 
    LEFT OUTER JOIN "users" ON "users"."id" = "items"."user_id" 
    WHERE (users.id IS NOT NULL)

能用,但你真的不应该这样做。当需要条件时,请使用条件语句。当不需要时,请相信Rails的默认行为。它已经被非常聪明的人密切审查过了。 - Matchu
是的,说得好。但这让我对他们做出的决定背后的原因感到兴趣。我认为解释会受到欢迎。我提出了这个问题,希望能够澄清。 - sinharaj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接