【问题标题】:Why is Rails ActiveRecord hitting the database twice?为什么 Rails ActiveRecord 会两次访问数据库?
【发布时间】:2023-03-05 12:20:01
【问题描述】:
@integration = Integration.first(:conditions=> {:integration_name => params[:integration_name]}, :joins => :broker, :select => ['`integrations`.*, `brokers`.*'])
$stderr.puts @integration.broker.id # This line causes Brokers to be queried again

结果:

Integration Load (0.4ms)   SELECT `integrations`.*, `brokers`.* FROM `integrations` INNER JOIN `brokers` ON `brokers`.id = `integrations`.broker_id WHERE (`integrations`.`integration_name` = 'chicke') LIMIT 1
Integration Columns (1.5ms)   SHOW FIELDS FROM `integrations`
Broker Columns (1.6ms)   SHOW FIELDS FROM `brokers`
Broker Load (0.3ms)   SELECT * FROM `brokers` WHERE (`brokers`.`id` = 1) 

任何想法为什么 Rails 会再次为 brokers 访问数据库,即使我已经加入/选择了它们?

这里是模型(代理 -> 集成是一对多的关系)。请注意,这是不完整的,我只包括了建立它们关系的行

class Broker < ActiveRecord::Base

  # ActiveRecord Associations
  has_many :integrations

class Integration < ActiveRecord::Base

  belongs_to :broker

我使用的是 Rails/ActiveRecord 2.3.14,所以请记住这一点。

当我执行Integration.first(:conditions=&gt; {:integration_name =&gt; params[:integration_name]}, :include =&gt; :broker) 时,该行会导致两个SELECTs

Integration Load (0.6ms)   SELECT * FROM `integrations` WHERE (`integrations`.`integration_name` = 'chicke') LIMIT 1
  Integration Columns (2.4ms)   SHOW FIELDS FROM `integrations`
  Broker Columns (1.9ms)   SHOW FIELDS FROM `brokers`
  Broker Load (0.3ms)   SELECT * FROM `brokers` WHERE (`brokers`.`id` = 1) 

【问题讨论】:

    标签: sql ruby-on-rails activerecord


    【解决方案1】:

    使用include 而不是joins 以避免重新加载Broker 对象。

    Integration.first(:conditions=>{:integration_name => params[:integration_name]}, 
      :include => :broker)
    

    没有必要给出select 子句,因为您没有尝试规范化brokers 表列。

    注 1:

    在急切加载依赖项时,AR 对每个依赖项执行一个 SQL。在您的情况下,AR 将执行 main sql + broker sql。由于您试图获得一排,因此没有太多收获。当您尝试访问 N 行时,如果您预先加载依赖项,您将避免 N+1 问题。

    注2:

    在某些情况下,使用自定义急切加载策略可能会有所帮助。让我们假设您只想获取集成的关联代理名称。您可以按如下方式优化您的 sql:

    integration = Integration.first(
      :select => "integrations.*, brokers.name broker_name",
      :conditions=>{:integration_name => params[:integration_name]}, 
      :joins => :broker)
    
    integration.broker_name # prints the broker name
    

    查询返回的对象将包含select 子句中的所有别名列。

    当您想要返回Integration 对象时,即使没有对应的Broker 对象,上述解决方案也不起作用。你必须使用OUTER JOIN

    integration = Integration.first(
      :select => "integrations.*, brokers.name broker_name",
      :conditions=>{:integration_name => params[:integration_name]}, 
      :joins => "LEFT OUTER JOIN brokers ON brokers.integration_id = integrations.id")
    

    【讨论】:

    • 该行导致发生两个查询(粘贴在上面)
    • @babonk,我已经用解释更新了我的答案。这是因为您只访问一行。将Integration.first( 更改为Integration.all(:limit =&gt; 5,您应该会看到 2 个 sql 而不是 6 个。
    • 我正在尝试进行 1 个查询,而不是 2 个。这是一个非常基本的 SQL 连接,为什么 rails 不能做到呢?我猜弗雷德里克有下面的解释..
    • 这是当前的 Rails 预加载植入。虽然我理解这种挫败感。我已经更新了我的答案,并附上了详细说明处理此类情况的可能方法的附加说明。
    【解决方案2】:

    :joins 选项只是让活动记录向查询添加连接子句。它实际上并没有使活动记录对返回的行做任何事情。关联未加载,因此访问它会触发查询

    :include 选项是关于提前加载关联的。活动记录有两种策略来做到这一点。一种是通过大连接查询,另一种是通过每个关联触发一个查询。默认是后者,这就是您看到两个查询的原因。

    在 rails 3.x 上,您可以通过 Integration.preload(:broker)Integration.eager_graph(:broker) 来决定您想要哪些策略。

    rails 2.x 中没有这样的工具,所以您唯一能做的就是欺骗用于确定策略的启发式方法。每当 rails 认为 order 子句、select 子句或条件引用了包含关联上的列时,它就会切换到 joins 策略(因为它是唯一在这种情况下有效的策略)。

    例如做类似的事情

    Integration.first(:conditions => {...}, :include => :broker, :select => 'brokers.id as ignored')
    

    应该强制使用替代策略(在这种情况下,活动记录实际上会忽略选择选项)。

    【讨论】:

    • 哇,所以强制使用 INNER JOIN 的唯一方法是“欺骗”Rails?太可怕了。
    • 好吧,如果你坚持使用 > 3 岁的版本......我发现当你只获取这样的一行时,包括与否并没有太大区别.
    猜你喜欢
    • 2020-03-11
    • 1970-01-01
    • 1970-01-01
    • 2016-01-25
    • 2012-04-25
    • 2014-12-06
    • 2012-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多