如何让Rails针对此迭代执行正确的单个SQL连接查询?

3

我基本没有Rails的经验,但我正在尝试帮助一个正在学习编程的朋友。虽然我有很多SQL的经验,但我仍然很难通过联接来实现对简单查询的正确操作。

我有两个表,drawingsdrawing_typesdrawings有一个外键指向drawing_types,它有一个type_description列。这显然是你能想象到的最基本的关系。在SQL中,我会使用以下代码来获取所有带有正确type_descriptions的图纸:

SELECT d.*, dt.type_description FROM drawings as d
INNER JOIN drawing_types as dt ON dt.id = d.drawing_type_id;

这是一个高效的SQL查询,可以准确地返回我想要的内容:绘图列表以及它们的文本类型描述。
但是,我无论如何都无法让Rails生成并使用这样一个单一的查询。我最多只能做到两个查询,这显然表明它在代码中执行“join”,而不是让数据库来执行。有时候它还会执行N+1个查询!?
以下是模型的代码:
class Drawing < ActiveRecord::Base
  attr_accessible :image, :drawing_type_id
  belongs_to :drawing_type
end

class DrawingType < ActiveRecord::Base
  attr_accessible :type_description
  has_many :drawings
end

这里是相关的控制器代码:

@drawings = Drawing.includes(:drawing_type)

这里是视图:

<% @drawings.each do |drawing| %>
  <tr>
    <td><b><%= drawing.drawing_type.type_description %></b></td>

这将生成两个SQL查询,一个查询 drawings.*,另一个查询所有描述,然后在代码中明确查找这些描述。如果我将includes更改为joins,它将执行1+M+N个SQL查询(其中M表示图纸类型数量,N表示图纸数量)!?!?!
我可以通过使用以下内容将正确的SQL连接记录到日志中来解决此问题(取自此处):
@drawings = Drawing.select("drawings.*, drawing_types.type_description").
     joins("INNER JOIN drawing_types ON drawing_types.id = drawings.drawing_type_id")

但是,Rails似乎只把结果作为一组绘图的数组使用,忽略结果中的type_description列,因此它会执行额外的M+N查询!
所有这些变化都“可行”,因为对于所有这些变化,页面都会正确呈现,但是相对于应该执行的单个正确SQL连接来说,它们都是错误的。
由于这是在一个非常无聊的网站模式中可能找到的最基本的关系之一,因此必须有一种简单的方法来正确地完成此操作,但是我已经四处寻找过了,包括阅读了这里的许多答案和所有文档,但无法弄清楚如何做到这一点。
谢谢, Chris
2个回答

2

我最近在工作中遇到了同样的问题,发现可以使用.joins().select()来解决。我发现最好的原型设计方法是在Rails控制台中尝试不同的操作,直到获得所需的查询结果。

控制器:

@drawings = Drawing.joins(:drawing_type).select([:name, :description]).all

这将生成:
Drawing Load (0.4ms)  SELECT name, description FROM `drawings`
  INNER JOIN `drawing_types` ON `drawing_types`.`id` = `drawings`.`drawing_type_id`

在我的例子中,DrawingType上有一个字段“description”,而不是“type_description”,但这应该是唯一的区别。
并且在视图中对应的代码为:
<% @drawings.each do |drawing| %>
  <tr>
    <%= content_tag, :td, drawing.name %>
    <%= content_tag, :td, drawing.description %>
  </tr>
<% end %>

1
啊,好的,这个按预期工作了(忽略它在第一次使用时是混淆且必要的),谢谢!另外,如果有任何来自未来的人阅读此文,请注意,我必须在select中使用字符串,因为我无法弄清如何消除共享列名(例如:id)的歧义,所以我做了.select(["drawings.*", :type_description])。此外,值得强调的是,此解决方案直接将属性放在返回的对象上,因此您可以通过drawing.type_description访问它,在正常的includes语法中,您需要使用drawing.drawing_type.type_description - Chris Hecker

1
这种行为是有意的,因为在许多情况下,使用两个查询比一个查询更快。请参见此答案,了解推理的良好解释:如何在Rails中避免使用:include进行多个查询? 尽管如此,您可以通过在关系上指定where条件来强制执行单个查询,但不建议这样做。

谢谢。我发现这相当令人惊讶,因为它似乎对于迭代结果行的方式做出了不可扩展的假设,正如该人的后续回答中所述(在此处查看)。where条件似乎是请求连接的一种奇怪方式,但我很感激这个回答,我会测试一下。 - Chris Hecker
所以,我尝试了一个简单的where语句,使用左表的一列,但它仍然执行了两个查询。但是,如果我使用右侧连接的表进行where语句,它会生成一个相当丑陋的单个SQL查询,但至少只有一个查询。但是,where语句的存在并不强制执行查询,只有在对连接表进行条件判断时才会填充查询,并使用一堆机器生成的AS名称将其连接起来。Marc在下面的建议中生成了更简单的查询,而且还正确地将连接属性直接放在对象上。 - Chris Hecker

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