为什么将我的 ActiveRecord 关系存储在一个记忆变量中不能按我预期的方式工作?

Why does storing my ActiveRecord Relation in a memoized variable not work the way I expected?

编辑:我正在使用 Ruby 2.3.3 和 Rails 5.2.4.3,仅供参考。

我有一个名为 Event 的 ActiveRecord 模型,我的数据库中有超过 700 万条记录。当我在我的 Rails 控制台中键入以下内容时,Rails 记录器告诉我发生了 I/O 查找,这大约需要 12.0 毫秒:

irb(main):006:0> @events = Event.where("id > 0")
  Event Load (12.0ms)  SELECT  `events`.* FROM `events` WHERE (id > 0) LIMIT 11

我的期望是,如果我使用 ||= 操作(例如 @events ||= 'foobar')有条件地将 @events 重置为另一个值,我会 not 看到第二条 Event Load 语句记录到屏幕上(因为 @events 已经存在,所以 ||= 意味着不需要计算表达式的第二部分) .然而,我确实看到了第二次查找:

irb(main):007:0> @events ||= 'foobar'
  Event Load (0.5ms)  SELECT  `events`.* FROM `events` WHERE (id > 0) LIMIT 11

诚然,查找速度要快得多(0.5ms vs 12.0ms),但 I/O 发生的事实让我感到困惑。我觉得我误解了一些关于 ActiveRecord 如何处理 ||= 语句的基本知识,但我不确定那是什么。

我的目标是将第一个 ActiveRecord 查询的结果缓存在实例变量中,这样对该实例变量的后续引用将不会调用任何类型的额外 I/O 调用,从而节省时间本来会花在这样的 I/O 电话上。

编辑: 这是我在 Rails 控制台中输入的完整命令序列的类似版本(这次使用我的应用程序的 Role 模型),以及删节后的结果:

irb(main):001:0> @roles = Role.where("id > ?", 0)
   (3.9ms)  SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci,  @@SESSION.sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Role Load (22.0ms)  SELECT  `roles`.* FROM `roles` WHERE (id > 0) LIMIT 11
=> #<ActiveRecord::Relation [#<Role id: 1, name: "Engineering Intern", account_id: 1, created_at: "2013-05-14 23:03:54", updated_at: "2013-05-14 23:03:54", deleted_at: nil>, #<Role id: 2, name: "Operations", account_id: 1, created_at: "2013-05-14 23:04:02", updated_at: "2013-05-14 23:04:02", deleted_at: nil>, 
......


irb(main):002:0> @roles ||= :foobar
  Role Load (0.4ms)  SELECT  `roles`.* FROM `roles` WHERE (id > 0) LIMIT 11
=> #<ActiveRecord::Relation [#<Role id: 1, name: "Engineering Intern", account_id: 1, created_at: "2013-05-14 23:03:54", updated_at: "2013-05-14 23:03:54", deleted_at: nil>, #<Role id: 2, name: "Operations", account_id: 1, created_at: "2013-05-14 23:04:02", updated_at: "2013-05-14 23:04:02", deleted_at: nil>, 
......

编辑: 我假设 Ruby 解释器读取 x ||= yx = x || y 的方式之间可能存在微妙的(至少对我而言)差异,所以我也尝试了@roles = @roles || :foobar,但我仍然看到 SQL 查询记录到 REPL。

我认为您看到的行为与控制台有关。如果您在 Rails 应用程序的上下文中执行此操作,它会按预期工作。例如,我有一个 Client 模型,我写了一个 `get_them_all' 方法,如下所示:

def self.get_them_all
  @clients = Client.where("id > 52000")
  puts "got them"
  @clients ||= "foobar"
  puts "still have them?"
  @clients
end

当我在 Rails 控制台中 运行 Client.get_them_all 时,我看到对数据库的单个查询。同样有趣的是,单个查询是 运行 两个 puts 语句之后。 Rails 仅在实际必须使用结果时才访问数据库。在此之前,它在@clients 变量中只有我称之为新生查询的内容。

此行为意味着您可以将 Client#get_them_all 方法与其他查询片段链接起来,因为它是一个 ActiveRecord::Relation。所以,在 rails 控制台中

$> Client.get_them_all.class.name #=> ActiveRecord::Relation, not Array
$> Client.get_them_all.where(lastName: 'Escobar') # I can append 'where'

load(&block) 应该可以解决你的问题。

Causes the records to be loaded from the database if they have not been loaded already. You can use this if for some reason you need to explicitly load some records before actually using them. The return value is the relation itself, not the records.

https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/Relation.html#method-i-load

@events = Event.where("id > 0").load
@events ||= 'foobar'