发布于 2026-01-06 1 阅读
0

Rails 5.2.x --> Rails 6 counter_cache Gotcha DEV 的全球展示挑战赛,由 Mux 呈现:Pitch Your Projects!

Rails 5.2.x --> Rails 6 counter_cache 问题

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

我最近将我们的生产应用从 Rails 5.2.3 升级到了 Rails 6.0.0。我们想要体验所有大家都在谈论的新功能:多数据库!更完善的自动加载!

一切都很顺利,直到我注意到一个奇怪的现象:计数列没有更新,但所有关联关系都正确更新了。和同事讨论这个问题后,我们找到了原因。罪魁祸首是?Rails ActiveRecord counter_cache

本文将简要概述counter_cache两个 Rails 版本之间的行为变化,并最终介绍如何更新代码以解决此问题,而无需彻底修改代码counter_cache

计数器缓存——它是什么?我为什么要使用它?

这个概念对我来说是全新的——太棒了!我喜欢学习新知识,也喜欢努力理解为什么这种方法比其他方法更好。这次的情况比较特殊,因为代码比较老旧,我们没法直接问原作者当初为什么这么做。所以,我只好上网查资料了。

我找到了一些关于如何实现的文章counter_cache,但令我惊讶的是,它们都不是来自 Rails 官方文档——这很有意思。

话虽如此,我认为我找到的最好的资源是Railscast 的一期(很老的)节目,内容是关于如何获取记录计数以及它与数据库中计数器缓存的关系。我强烈推荐观看——但这里简单解释一下为什么要实现计数器缓存:

计数器缓存列存储与其自身关联的计数——这意味着您无需进行额外的数据库调用,从而提高了性能。

旧行为与实施

假设我们有一个饼干罐,每个饼干罐里都装着许多饼干。我们希望在用户界面中显示每个饼干罐里有多少块饼干。

模型设置的简单实现方式可能如下所示:

# cookie_jar.rb
class CookieJar
  has_many :cookies
end

# ## Schema Information
# Table name: `cookie_jars`
# ### Columns
#
# Name                 | Type               | Attributes
# -------------------- | ------------------ | ---------------
# **`id`**             | `bigint(8)`        | `not null, primary key`
# **`cookies_count`**  | `integer`          | `default(0), not null`

# cookie.rb
class Cookie
  belongs_to :cookie_jar, counter_cache: true
end

# ## Schema Information
# Table name: `cookies`
# ### Columns
#
# Name                 | Type               | Attributes
# -------------------- | ------------------ | ---------------
# **`id`**             | `bigint(8)`        | `not null, primary key`
# **`cookie_jar_id`**  | `integer`          | `not null`

所以这意味着我们现在有了CookieJarCookie并且该cookie_jars表有一个名为的列cookies_count,用于counter_cache计算这些值。

现在棘手的部分来了——counter_cache在后台进行一些工作,这使得它易于使用,但调试起来却更具挑战性。在这种情况下,我们的错误根源在于一些如下所示的初始代码:

# seed_cookie_jar.rb
def assign_cookies
  jar = CookieJar.first
  jar.cookies.each do |cookie|
    cookie.update!(cookie_jar_id: jar.id)
  end
end

需要注意的是,在这个例子中,我们不必关心 cookie 是在哪里创建的——只需要知道它们之前有不同的值,cookie_jar_id而我们想在这里更改它们assign_cookies

所以,在 Rails 6 之前,这种行为是有效的——我将我的第一个 cookie 从一个 jar 移动到另一个 jar,旧 jar 现在少了一个 cookie cookies_count,而新 jar 的 cookie 数量增加了cookies_count

在我们打电话之前,我们的饼干罐桌子assign_cookies

ID cookies_count
1 4
2 0

我们把饼干搬动后,饼干罐桌子就变成了这样。

ID cookies_count
1 0
2 4

一切正常!我们的 4 个 cookie 都从 Cookie Jar 1 转移到了 Cookie Jar 2。太棒了!现在让我们好好折腾一番,升级到 Rails 6 吧 💪

新的行为和改变

平心而论,变更日志中确实有一行提到了实现新功能的拉取请求。然而,令我惊讶的是,变更日志中对这一改动的描述方式完全没有让我意识到我的代码实际上已经失效了。

以下是更新日志的说明:“除非记录实际已保存,否则不要更新计数器缓存”。没错,就是这样。我不是 Ruby 或 Rails 专家,所以对其他人来说这或许是显而易见的……但天哪,它着实让我摸不着头脑!

在不修改代码的情况下,我们遇到的错误是这样的:
调用之前的 Cookie Jar 表assign_cookies(和上次一样)

ID cookies_count
1 4
2 0

调用后,我们的 Cookie Jar 表看起来是这样的assign_cookies——没有错误,但是这些计数看起来不太对劲 🤔

ID cookies_count
1 4
2 0

更令人困惑的是,在我们移动 cookie 之后,我们的 Cookies 表看起来是这样的:

ID cookie_jar_id
1 2
2 2
3 2
4 2

什么?!我们的饼干真的换罐子了,但是计数器却没更新。所以,在我们的用户界面上,显示的是“饼干罐 1”里有 4 块饼干,但是当你点击查看“饼干罐 1”的详细信息时,它却是空的!

说实话,我和同事找到解决方法的方法是:找到变更日志中提到的修改代码的 pull request,查看代码差异、新增的测试用例,然后进行一些尝试。最终,我们找到了一种方法:不传递 id 值,而是将整个记录传递给代码块。(剧透一下——这招奏效了!)以下是相关代码:

# seed_cookie_jar.rb
def assign_cookies
  jar = CookieJar.first
  jar.cookies.each do |cookie|
    cookie.update!(cookie_jar: jar)
  end
end

所以不再cookie_jar_id直接设置——我们把整个 jar 实例传递给它,看看会发生什么。结果发现,这确实会调用jar实例的保存操作,并触发缓存更新。


感谢您一路陪伴我走到今天——希望您有所收获,并且现在在更新关联字段时更加谨慎!从今以后,我一定会三思而后行,在传递显式值而不是实例_id之前,务必三思。_id


照片由Brooke Lark 拍摄,来自 Unsplash

文章来源:https://dev.to/loribbaum/rails-5-2-x-rails-6-countercache-gotcha-3bgc