【问题标题】:Rails 3.1 scope ignored when joining twice to the same table两次加入同一个表时,Rails 3.1 范围被忽略
【发布时间】:2012-11-19 02:36:52
【问题描述】:

使用 rails 3.1.4 在不同的范围内两次加入同一个表时遇到问题。其中一个范围被完全忽略,包括 join 和 where 子句。此删除操作不会出现错误或通知。

这是导致问题的简化示例:

Task 是标准 Rails 模型,与模型 SavedOutput 具有多态关系。 SavedOuput 用作缓存来存储复杂方法的结果。

任务模型如下所示:

class Task < ActiveRecord::Base
  has_many :saved_outputs
  scope :saved_lates, lambda { joins(:saved_outputs).where(
    "saved_outputs.method" => "late?",
    "saved_outputs.output" => true
  )}
  scope :saved_completes, lambda { joins(:saved_outputs).where(
    "saved_outputs.method" => "complete?",
    "saved_outputs.output" => true
  )}
  ...

有了这段代码,我可以调用Task.saved_lates 而不是调用Task.all.select(&amp;:late?) 之类的东西,假设缓存的数据是最新的。

问题是调用Task.saved_lates.saved_completes 不起作用。我相信 Rails 的重复查询检测会启动并删除第二个范围。即使没有发生这种情况,查询仍然会失败,因为如果不使用 MYSQL 中的别名,您将无法两次连接到同一个表。

我有一个手动编写的联接和表别名的部分解决方案。

  scope :saved_lates, lambda { joins("INNER JOIN saved_outputs AS so1 ON so1.object_type='Task' AND so1.object_id=tasks.id").where(
    "so1.method" => "late?",
    "so1.output" => true
  )}
  scope :saved_completes, lambda { joins(INNER JOIN saved_outputs AS so2 ON so2.object_type='Task' AND so2.object_id=tasks.id).where(
    "so2.method" => "complete?",
    "so2.output" => true
  )}

这个解决方案的问题是别名so1 需要在整个项目中是唯一的。考虑到 SavedOutput 模型保存了许多不同模型的输出,我需要使用全局唯一 id 或唯一哈希系统来标记别名。

是否有解决方案以静默方式从查询中删除范围?
有没有办法强制 rails 在每个标准连接上创建唯一的表别名?
使用带有关联符号作为参数的连接范围是不好的做法吗?


这是我正在查看的完整示例:

class ScopeTest < ActiveRecord::Migration
  def change
    create_table :foos do |t| 
      t.string :name
      t.boolean :active, :default => true
    end 

    create_table :bars do |t| 
      t.integer :foo_id
      t.boolean :method_value
    end 
  end
end

class Foo < ActiveRecord::Base
  has_many :bars
  scope :ones, lambda { joins(:bars).where("bars.method_value" => true) }
  scope :zeroes, lambda { joins(:bars).where("bars.method_value" => false) }
end

运行范围和链接范围的结果:

irb(main):022:0> Foo.ones.to_sql
=> "SELECT `foos`.* FROM `foos` INNER JOIN `bars` ON `bars`.`foo_id` = `foos`.`id` WHERE `bars`.`method_value` = 1"
irb(main):023:0> Foo.zeroes.to_sql
=> "SELECT `foos`.* FROM `foos` INNER JOIN `bars` ON `bars`.`foo_id` = `foos`.`id` WHERE `bars`.`method_value` = 0"
irb(main):024:0> Foo.ones.zeroes.to_sql
=> "SELECT `foos`.* FROM `foos` INNER JOIN `bars` ON `bars`.`foo_id` = `foos`.`id` WHERE `bars`.`method_value` = 0"
irb(main):025:0> Foo.zeroes.ones.to_sql
=> "SELECT `foos`.* FROM `foos` INNER JOIN `bars` ON `bars`.`foo_id` = `foos`.`id` WHERE `bars`.`method_value` = 1"

当我链接查询时,我想从两个范围中获取结果的交集。我希望 sql 的含义与此相同:

SELECT `foos`.* from `foos` 
INNER JOIN `bars` AS `bars1` ON `bars1`.`foo_id` = `foos`.`id` 
INNER JOIN `bars` AS `bars2` ON `bars2`.`foo_id` = `foos`.`id`
WHERE `bars1`.`method_value` = 1 AND `bars2`.`method_value` = 0

我该怎么做?

【问题讨论】:

  • 我认为问题出在你所建议的那样,当作用域被链接时,where 和 join 子句没有合并,而是被替换(我认为这在 Rails 4 中发生了变化)。做第三个瞄准镜就这么可怕吗?
  • Tom,有很多简单的一次性解决方案可以解决这个问题。连接表上带有别名的版本很笨拙,但可以按我想要的方式工作。在这一点上,我正在为需要两个连接到同一个表的查询寻找更通用的“rails 方式”解决方案。我刚刚在问题中添加了一个带有工作代码的完整示例。请看一下,如果它澄清了我的问题,请告诉我。

标签: sql ruby-on-rails activerecord ruby-on-rails-3.1 scope


【解决方案1】:

你怎么知道它不起作用? SQL 说正在发生什么?我只是模拟了一个模型...

class Foo < ActiveRecord::Base
  has_many :bars
  scope :one, lambda { joins(:bars).where('x = 1') }
  scope :two, lambda { joins(:bars).where('x = 2') }
end

然后在控制台中运行它......查询看起来很好......当然,它不会返回任何结果,因为条件不可能满足,但一切都像我预期的那样被合并/链接。

Foo.one.two.to_sql
 => "SELECT `foos`.* FROM `foos` 
     INNER JOIN `bars` ON `bars`.`foo_id` = `foos`.`id` 
     WHERE (x = 1) AND (x = 2)"

这适用于 Rails 3.2.8...

【讨论】:

  • 感谢您的评论。明天我将尝试在 rails 3.2 上进行测试,看看是否能得到更好的结果。我对您的示例的问题是它返回的 sql 不是我所期望的。它返回所有带有 x=1 和 x=2 的条的 foo。我正在寻找所有带有 x=1 的条和另一个带有 x=2 的条的 foos。在我看来,Foo.one.two 应该返回 Foo.one 和 Foo.two 的交集。在这个例子中它没有。有没有不同的方法来编码以获得我正在寻找的东西?
  • 链接范围是一个 AND 的 sql 条件,而不是一个交集。你想要Foo.one + Foo.two
  • 两个条件 AND 一起的结果应该是它们结果的交集。如果 Foo.one 返回[a, b, c] 并且 Foo.two 返回[b, c, d],则 Foo.one.two 应该是交集,[b, c]。这通常是正确的,但令人惊讶的是,在您给出的示例中并非如此。我用一个具体的例子编辑了我原来的问题。请看一下,看看它是否能澄清我的问题。
  • 不,不一定。如果这两个条件创建了一个不可能的条件.. 说 x = 1 AND x = 2 那么你不会得到任何行回来。您的原始查询要求“方法”等于“迟到?”和“完成?”。这将导致不返回任何行,因为一行不可能同时满足这两个条件。如果你真的只想要一个 SQL 查询,你需要重写它以使用“方法 IN ('late?', 'complete?')”或劫持 AR 并使用 SQL 的 UNION。可能是 UNION 在 Arel 的某个地方可用,我不知道......
  • 我认为你不明白我的问题。我想要一个查询,它返回一个带有 x=1 的条和另一个带有 x=2 的条的 Foos。我正在尝试找出执行此操作的“导轨方式”。这是返回我想要的结果的手写查询:SELECT 'foos'.* from 'foos' INNER JOIN 'bars' AS 'bars1' ON 'bars1'.'foo_id' = 'foos'.'id' INNER JOIN 'bars' AS 'bars2' ON 'bars2'.'foo_id' = 'foos'.'id' WHERE 'bars1'.'x' = 1 AND 'bars2'.'x' = 2
猜你喜欢
  • 1970-01-01
  • 2017-07-06
  • 1970-01-01
  • 2011-11-08
  • 1970-01-01
  • 2018-06-23
  • 2023-03-20
  • 1970-01-01
  • 2012-05-29
相关资源
最近更新 更多