homeASCIIcasts

181: Include vs Joins 

(view original Railscast)

Other translations: En It

Other formats:

Written by Calf

ActionRecord中的find方法可以带有几个不同的选项。在这集,我们将关注includejoins选项。我们常常会感到困惑,因为它们执行的是非常相似的任务,但是在它们之间做出正确选择是非常关键的。

我们用一个Rails程序说明includejoins之间的区别,在这个程序中用户可以添加文章。下面的两个model是通过标准的has_many/belongs_to关系关联起来的。

class User < ActiveRecord::Base  
  has_many :comments  
end  
class Comment < ActiveRecord::Base  
  belongs_to :user  
end  

程序有一个主页,显示的是已有文章。每个文章的作者也被显示出来,如果作者是管理员身份,那么后面会带有”(admin)”标识。

The application's comments page.

现在我们想要在这个页面上只显示由管理员用户写的文章。所以需要更改CommentsController中的index action。

def index  
  @comments = Comment.all(:order => "comments.created_at desc")
end  

我们想要通过users表中的一个属性来过滤comments,所以必须在comments的find查询中就关联上user表,把users加入到一个单一的查询中。但是这里我们应该用joins还是include呢?为了解决这个问题,我们现在console中比较这两个选项。

首先尝试joins选项,添加一个条件这样find方法只返回管理员用户。

>> c = Comment.all(:joins => :user, :conditions => { :users => { :admin => true } })  Comment Load (1.2ms)   SELECT "comments".* FROM "comments" INNER JOIN "users" ON "users".id = "comments".user_id WHERE ("users"."admin" = 't')
+----+-----------------------+---------+-----------------------+-----------------------+
| id | content               | user_id | created_at            | updated_at            |
+----+-----------------------+---------+-----------------------+-----------------------+
| 3  | Some people, when ... | 1       | 2009-09-28 19:00:3... | 2009-09-28 19:00:3... |
| 5  | Write the code as ... | 2       | 2009-09-28 19:44:0... | 2009-09-28 19:44:0... |
| 6  | Walking on water a... | 1       | 2009-09-28 19:46:2... | 2009-09-28 19:46:2... |
| 8  | It should be noted... | 2       | 2009-09-28 19:49:3... | 2009-09-28 19:49:3... |
+----+-----------------------+---------+-----------------------+-----------------------+
4 rows in set

上面执行的单一查询仅仅从comments表中返回几个属性。如果我们想要得到第一个文章的作者,那么必须执行另一个数据库查询,因为上面的查询并没有的到users的属性。

>> c.first.user
  User Load (0.3ms)   SELECT * FROM "users" WHERE ("users"."id" = 1)
+----+--------+-------+-------------------------+-------------------------+
| id | name   | admin | created_at              | updated_at              |
+----+--------+-------+-------------------------+-------------------------+
| 1  | Eifion | true  | 2009-09-28 18:51:53 UTC | 2009-09-28 18:51:53 UTC |
+----+--------+-------+-------------------------+-------------------------+
1 row in set

So let’s try doing the same thing again, but with include instead of joins.

>> c = Comment.all(:include => :user, :conditions => { :users => { :admin => true } })
  Comment Load Including Associations (0.7ms)   SELECT "comments"."id" AS t0_r0, "comments"."content" AS t0_r1, "comments"."user_id" AS t0_r2, "comments"."created_at" AS t0_r3, "comments"."updated_at" AS t0_r4, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."admin" AS t1_r2, "users"."created_at" AS t1_r3, "users"."updated_at" AS t1_r4 FROM "comments" LEFT OUTER JOIN "users" ON "users".id = "comments".user_id WHERE ("users"."admin" = 't')
+----+-----------------------+---------+-----------------------+-----------------------+
| id | content               | user_id | created_at            | updated_at            |
+----+-----------------------+---------+-----------------------+-----------------------+
| 3  | Some people, when ... | 1       | 2009-09-28 19:00:3... | 2009-09-28 19:00:3... |
| 5  | Write the code as ... | 2       | 2009-09-28 19:44:0... | 2009-09-28 19:44:0... |
| 6  | Walking on water a... | 1       | 2009-09-28 19:46:2... | 2009-09-28 19:46:2... |
| 8  | It should be noted... | 2       | 2009-09-28 19:49:3... | 2009-09-28 19:49:3... |
+----+-----------------------+---------+-----------------------+-----------------------+
4 rows in set

这次执行了一个更复杂的SELECT查询,而且从comments和users表中都得到了相关的列记录。相关的User模型被存到了内存中,所以我们可以这次我们不需要查询数据库就可以获得第一个文章的作者。

>> c.first.user
+----+--------+-------+-------------------------+-------------------------+
| id | name   | admin | created_at              | updated_at              |
+----+--------+-------+-------------------------+-------------------------+
| 1  | Eifion | true  | 2009-09-28 18:51:53 UTC | 2009-09-28 18:51:53 UTC |
+----+--------+-------+-------------------------+-------------------------+
1 row in set

改变评论页面

既然我们已经对includejoins之间的差别有了一定的了解,那么在这个文章页面中应该选择哪一个呢?我们要考虑的是“是否用到相关model中的一些属性”。现在的情况,答案是“Yes”,因为我们要显示每个文章的作者。这意味着我们想要在得到文章的同时还要取得它的作者,所以这里应该用include

回到CommentsController,我们要修改index action来获得文章和它的用户。

def index  
  @comments = Comment.all(:include => :user, :conditions => { :users => { :admin => true} }, :order => "comments.created_at desc")  
end

现在这个查询的方法看上去还有一点复杂,我们可能想到把它移到一个命名好了的范围中去或者把它当做一个单独的程序。但是我们暂不用管它。

如果情况改变一点点,我们怎样才能不把用户名显示在页面上呢?让我们看看。首先把comment局部模板中显示用户名(以及是否为管理员)的部分去掉。

<div class="comment">
  <%= simple_format comment.content %>
  <p class="author">
    <%= h comment.user.name %>
    <% if comment.user.admin? %>(admin)<% end %>
  </p>
  <p class="actions">
    <%= link_to "edit", edit_comment_path(comment) %> |
    <%= link_to "destroy", comment, :method => :delete, :confirm => "Are you sure?" %>
  </p>
</div>

现在从新加载页面,用户名消失了。

The comments page with the users’s names removed.

我们没有在页面上显示任何用户信息,所以现在看来 include 选项是相当低效的,因为我们从数据库中查询了相关用户的所有信息却没有使用。在这种情况下正确的选择应该是joins,这样我们就不会从数据库取出我们不需要的用户信息。现在我们需要做的是把include改成joins

def index  
  @comments = Comment.all(:joins => :user, :conditions => { :users => { :admin => true} }, :order => "comments.created_at desc")  
end

这样我们只是用users表来提供查询条件,所以文章页面变得更高效和更节约内存。

另外一个例子

让我们再看看刚才执行查询方法中的include选项产生的SQL语句。

SELECT "comments"."id" AS t0_r0, "comments"."content" AS t0_r1, "comments"."user_id" AS t0_r2, "comments"."created_at" AS t0_r3, "comments"."updated_at" AS t0_r4, "users"."id" AS t1_r0, "users"."name" AS t1_r1, "users"."admin" AS t1_r2, "users"."created_at" AS t1_r3, "users"."updated_at" AS t1_r4 FROM "comments" LEFT OUTER JOIN "users" ON "users".id = "comments".user_id WHERE ("users"."admin" = 't')

这个语句十分复杂,因为它把comments表和users表中所有的列都取出来,并且重新命名。这意味着include选项并没有与select产生作用,因为我们不能控制怎么SELECT语句的开头部分。如果你需要控制SELECT语句选择哪一列,那么你应该用joins选项。

那么在哪些情况下这些知识会有用呢?在我们的用户页面,该页面显示了所有用户和他们写的所有文章数量。

The users page.

在这个页面的视图文件,文章数量通过下面的代码来显示。

<%= pluralize user.comments.count, "comment" %>  

这些代码将为列表中的每个用户执行一次查询,这样做并不理想。如果当我们得到用户余下的信息的同时就获取了文章数量会更好。

我们可以通过同时使用joinsselect选项来达到目的。让我们用console来说明应该怎么做。

这次我们用joins选项关联users表和comments表,取得所有用户。用select选项来选择users表中所有列和文章数量,同时通过ActiveRecord提供的用户id分组查询结果。

>> User.all(:joins => :comments, :select => "users.*, count(comments.id) as comments_count", :group => "users.id")

以上我们得到了用户的详细信息和每个用户所写的文章数量。

User Load (1.3ms)   SELECT users.*, count(comments.id) as comments_count FROM "users" INNER JOIN "comments" ON comments.user_id = users.id GROUP BY users.id
+----+--------+-------+------------------+------------------+----------------+
| id | name   | admin | created_at       | updated_at       | comments_count |
+----+--------+-------+------------------+------------------+----------------+
| 1  | Eifion | true  | 2009-09-28 18... | 2009-09-28 18... | 2              |
| 2  | Susan  | true  | 2009-09-28 18... | 2009-09-28 18... | 2              |
| 3  | Paul   | false | 2009-09-28 18... | 2009-09-28 18... | 3              |
| 4  | John   | false | 2009-09-28 18... | 2009-09-28 18... | 1              |
+----+--------+-------+------------------+------------------+----------------+
4 rows in set

既然我们可以在一次查询中就可以得到所有用户和他们所写的文章数量,那么就可以修改users 的index页面了。我们仅仅需要做的是两个小小的改变,在这个控制器中用新的find方法替换User.all

def index
  @users = User.all(:joins => :comments, :select => "users.*, count(comments.id) as comments_count", :group => "users.id")
end

index视图里我们可以用comments_count来显示每个用户所写的文章数量,

<%= pluralize user.comments_count, "comment" %>  

重新加载时整个页面看上去并没有什么改变,但是现在更高效了因为它访问数据库时只执行了一次查询。

另外一个使用joins的例子

在这次视频的最后,我们将展示另一个使用joins而不是include的情况。下面我们看到UserComment模型,还有GroupMembership模型。

class Group < ActiveRecord::Base  
  has_many :memberships  
  has_many :users, :through => :memberships  
end  
  
class Membership < ActiveRecord::Base  
  belongs_to :user  
  belongs_to :group  
end  
  
class User < ActiveRecord::Base  
  has_many :memberships  
  has_many :groups, :through => :memberships  
  has_many :comments  
end  
  
class Comment < ActiveRecord::Base  
  belongs_to :user  
end  

在这里的设置中UserGroup之间通过Membership组成多对多关系。我们想要显示特定组中用户所写的文章。自然我们想要把Group和Comment关联起来,可能像这样。

class Group < ActiveRecord::Base  
  has_many :membership  
  has_many :users, :through => :memberships  
  has_many :comments, :through => :users  
end 

但是Ruby不支持这样嵌套的has_many :through关联,所以我们将寻找另一种途径,很高兴的是在这里我们又可以用到joins选项。

这是到现在为止我们所完成的页面,控制器GroupController的显示视图。我们有一列组成员,但是没有把他们的文章加上去。

The groups page.

我们又回到console来设计出显示文章的代码。首先要得到我们的group。

>> g = Group.first
  Group Load (0.4ms)   SELECT * FROM "groups" LIMIT 1
+----+------------------+-------------------------+-------------------------+
| id | name             | created_at              | updated_at              |
+----+------------------+-------------------------+-------------------------+
| 1  | Musician's Guild | 2009-10-01 20:09:11 UTC | 2009-10-01 20:09:11 UTC |
+----+------------------+-------------------------+-------------------------+
1 row in set

然后我们用joins来取得每组成员得文章。我们需要关联usersmemberships表,这样就可以把UserMemberships关联起来。接着我们添加条件把memberships限制在group_id和我们给出的组id一样的范围中。

>> Comment.all(:joins => { :user => :memberships },
:conditions => { :memberships => { :group_id => g.id } } )
  Comment Load (0.7ms)   SELECT "comments".* FROM "comments"
INNER JOIN "users" ON "users".id = "comments".user_id
INNER JOIN "memberships" ON memberships.user_id = users.id
WHERE ("memberships"."group_id" = 1)
+----+--------------------+---------+--------------------+--------------------+
| id | content            | user_id | created_at         | updated_at         |
+----+--------------------+---------+--------------------+--------------------+
| 1  | I have always w... | 3       | 2009-09-28 18:5... | 2009-09-28 18:5... |
| 3  | Some people, wh... | 1       | 2009-09-28 19:0... | 2009-09-28 19:0... |
| 4  | Java is to Java... | 3       | 2009-09-28 19:0... | 2009-09-28 19:0... |
| 5  | Write the code ... | 2       | 2009-09-28 19:4... | 2009-09-28 19:4... |
| 6  | Walking on wate... | 1       | 2009-09-28 19:4... | 2009-09-28 19:4... |
| 7  | Never trust a c... | 3       | 2009-09-28 19:4... | 2009-09-28 19:4... |
| 8  | It should be no... | 2       | 2009-09-28 19:4... | 2009-09-28 19:4... |
+----+--------------------+---------+--------------------+--------------------+
7 rows in set

执行查询后显示了该组用户的所有文章,所以我们就可以用它来完成group页面。

我们将在Group模型中的一个新的comments方法中使用到它。

class Group < ActiveRecord::Base  
  has_many :memberships  
  has_many :users, :through => :memberships  
  
  def comments  
    Comment.all(:joins => { :user => :memberships}, :conditions => { :memberships => { :group_id => id } } )  
  end  
end  

我们还需要在页面中显示comments,所以必须更新视图。我们已经有了一个comment局部模板,所以需要做的只是渲染(render)所有文章。

<h2>Comments</h2>  
<%= render @group.comments %>  

如果重新加载页面,我们将会在用户列表下面看到所以文章。

The groups page with the comments added.

再看看Group模型的comments方法,它似乎仍然有效如果我们用include替换joins,经常这两个选项好像是可以互换的。记住尽管在这里用include会把我们不需要的user和memberships也加载到内存中。

Comments方法建立了另一种关联,在这样的情况下我们可以用scoped来替代all。这样就像一个named scope一样,不同的是前者是动态生成的。优点是我们可以连锁使用其他scope去更缩小搜索范围,并且改变find的限制条件。

如果你觉得这次集有用,你可能想获得更多关于执行ActiveRecord查询的信息。

Ryan Bates制作了一系列叫“Everyday Active Record”的视频教程,那里将更深入地讲到我们提到的内容。