Rails Rails 6.1 升级到 7 的一个迷之错误,居然是缓存导致的 :) (以及另外三个问题)

hegwin · 2022年11月24日 · 最后由 hegwin 回复于 2023年05月25日 · 682 次阅读

从 Rails 6.1 升到 7 的时候,有仔细读过 ugprade guideline,但还是在部署之后遇到了一个很奇妙的错误,霸占了 Sentry…

ActionView::Template::Error
The :mode option must be one of [:all, :n_plus_one_only].

strict_loading是 6.1 加的一个功能,用来检测 N+1 query 的,但是当时直接忽略了它,默认全局关闭,因为有用别的 gem 做了这件事。。

等到了 7 遇到这个错误,尝试 debug 还翻到了这个错误的源码所在地,总之非常不理解:

# activerecord/lib/active_record/core.rb
def strict_loading!(value = true, mode: :all)
  unless [:all, :n_plus_one_only].include?(mode)
    raise ArgumentError, "The :mode option must be one of [:all, :n_plus_one_only] but #{mode.inspect} was provided."
  end

  @strict_loading_mode = mode
  @strict_loading = value
end

最终没有在 stack overflow 找到原因,而是在 rails 的 PR 里看到了这条 comment

解决方法是:清理掉 rails 的缓存就可以了。

讲道理,修改代码后,缓存的模型和新代码不兼容,这本来是一件挺正常的事情,但是这个错误提示实在是让人费解。

猜测原因可能是在 view 里缓存了一部分 active records,而这些缓存是在升 7 之前缓存的…当从缓存里拿出来这些 ActiveRecord[] 的时候,它们没有 mode 这个属性,于是就报错了。

其他小问题

  • Rails 6.x 及之前的 model_instance.errors.messages(Hash)没有 frozen 是可以自由修改的,而 Rails7 之后的 errors.messages 是一个 frozen hash。可能影响到的场景是,注册用户时,email 已经被注册了,我们这时候不是提示 email 的 error,而是引导用户去登录…但如果有其他 validation 失败,还是展示给用户看(emm 这逻辑有点奇妙是吗)。
# Rails 6.1

u = User.new
u.valid?
# => false
u.errors.messages
# => {:email=>["can't be blank"]}
u.errors.messages.frozen?
=> false

# Rails 7.0.4
u = User.new
u.valid?
# => false
u.errors.messages.frozen?
=> true
  • PG 的 created_at 类型,Rails 7 里变成了 TimeWithZone

详见这个 Issue Regression: Filtering a timestamp with timezone by date range 和这个 question

  • 终于向 PG 学习,MySQL 在转化带变量的 LIKE 语句有了一致的(令人不爽的)行为
prefix = "brand"

# 各种Rails版本下的的PG
Model.where("friendly_id like '%?-%'", prefix).to_sql
=>  "WHERE (friendly_id like '%'brand'-%')"

# Rails 6 下的MySQL
Model.where("friendly_id like '%?-%'", prefix).to_sql
=> "WHERE (friendly_id like '%brand-%')"

# Rails 7下的MySQL
Model.where("friendly_id like '%?-%'", prefix).to_sql
=>  "WHERE (friendly_id like '%'brand'-%')"

于是就不得不这么写

Model.where("friendly_id like ?", "%#{prefix}-%")

还有一个坑就是 ActiveSupport::TimeWithZone 对象的 to_s 方法的行为改了,不读取 Time::DATE_FORMATS[:default] = '%Y-%m-%d %H:%M:%S' 这个配置了,导致页面上所有的时间格式都不对了(created_at, updated_at) 解决办法就是要重载一下 to_s 方法

class ActiveSupport::TimeWithZone
  def to_s
    to_fs
  end
end

感谢,今天在 production 升级出了这个问题,搜到这个帖子光速解决了😄

goofansu 回复

蛤蛤蛤 能帮到人就好

需要 登录 后方可回复, 如果你还没有账号请 注册新账号