在我们的模型之一中,我们使用 ActiveRecord 的存储,如下所示:
store :settings, accessors: %i[enabled unit_of_distance], coder: JSON
这允许我们像普通列一样访问这些访问器,例如
Model.first.enabled => true
Model.first.unit_of_distance => 'miles'
如果我想像这样更新所有模型:
Model.update_all(unit_of_distance: 'miles')
我收到错误:
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'model.unit_of_distance' in 'field list'
像这样用
settings
前缀可以修复它,但随后会覆盖 settings
中的其他访问器(这根本不是我们想要发生的情况):
Model.update_all(settings: { unit_of_distance: 'miles' })
但是手动循环它们确实有效:
Model.all.each { |c| c.update(unit_of_distance: 'miles') }
但这可能效率不高...是否可以将
update_all
与存储访问器一起使用而无需执行循环?
这里有一个漏洞抽象与一把巨大的脚枪相结合。
ActiveRecord 试图跨越基于表的数据库世界和应用程序中的对象之间的鸿沟,但像任何 ORM 一样会遇到挫折。
update_all
是在 SQL 世界中操作的方法之一,而不是像 update
那样在对象上操作。因此,期望它采用哈希值来更新 JSON 列必然会导致失望。 store
是一把脚枪,它将序列化数据存储为字符串,然后在应用程序中将其反序列化 - 在面向对象的世界中。
store
是 RDMBS 系统拥有 JSON 和 JSONB 等结构化数据类型之前黑暗时代的一个老黑客。如果将其与本机 JSON 列一起使用,您将存储垃圾数据,因为您得到的是转义字符串而不是 JSON 对象。它带有以下警告:
注意:如果您使用结构化数据库数据类型(例如 PostgreSQL hstore/json,或 MySQL 5.7+ json)不需要 .store 提供的序列化。只需使用 .store_accessor 即可 生成访问器方法。
解决问题的第一步是修复表中的任何现有数据,并将存储的字符串转换为可以由数据库查询的实际 JSON。您可以使用
update_all
和 数据库 JSON 函数 来完成此操作,或者如果性能不是问题的话,可以通过循环遍历 Ruby 中的数据来完成此操作。
如果您想坚持将应该是列的内容放入 JSON 列中,您将需要使用 MySQL 特定的 JSON 运算符。
Model.update_all('model.settings = JSON_MERGE_PATCH(
model.settings, \'{"unit_of_distance": "miles" }\'
)')
如果您的数据足够结构化,以至于您可以在模型中为其创建访问器,那么它的非结构化程度不足以保证使用 JSON。