升级到 Rails 5 时,序列化为哈希的现有数据会产生错误
Posted
技术标签:
【中文标题】升级到 Rails 5 时,序列化为哈希的现有数据会产生错误【英文标题】:Existing data serialized as hash produces error when upgrading to Rails 5 【发布时间】:2018-07-24 05:37:10 【问题描述】:我目前正在将 Ruby on Rails 应用程序从 4.2 升级到 5.0,并且在将数据存储为序列化哈希的字段方面遇到了障碍。例如,我有
class Club
serialize :social_media, Hash
end
在创建新俱乐部和输入社交媒体时,一切正常,但对于现有的社交媒体数据,我得到:
ActiveRecord::SerializationTypeMismatch: Attribute 应该是 Hash,但实际上是 ActionController::Parameters。
如何将所有现有数据从ActionController::Parameter
对象转换为简单哈希?数据库是mysql。
【问题讨论】:
您使用的是哪个数据库?我问这主要是因为serialize
可能有更好的选择,而且由于无论如何你都要修复和重写所有这些数据,这可能是完全摆脱serialize
的好时机。
目前正在使用myql。
【参考方案1】:
根据short的回复扩展-不需要数据库迁移的解决方案:
class Serializer
def self.load(value)
obj = YAML.load(value || "")
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
value = if value.respond_to?(:to_unsafe_h)
value.to_unsafe_h
else
value
end
YAML.dump(value)
end
end
serialize :social_media, Serializer
现在club.social_media
无论是在 Rails 4 上还是在 Rails 5 上创建都可以使用。
【讨论】:
感谢@jpw - 我编辑了我的答案来处理 nil 的情况。【参考方案2】:在 Rails 4 上运行迁移,为 Rails 5 准备数据。
我们正在经历完全相同的事情,除了我们序列化为ActiveSupport::HashWithIndifferentAccess
而不是我建议这样做的Hash
,但我将在这里提供我的答案,只是一个简单的Hash
.
如果您还没有升级到 Rails 5,我希望您还没有升级,并且您的测试已经发现了这个问题,您可以在 Rails 4 分支上运行迁移,以便为 Rails 5 准备好数据。
它实质上将所有记录从ActionController::Parameters
重新序列化为Hash
,而在Rails 4 中ActionController::Parameters
仍然继承自HashWithIndifferentAccess
。
class ConvertSerializedActionControllerParametersToHashInClubs < ActiveRecord::Migration
disable_ddl_transaction! # This prevents the locking of the table (e.g. in production).
def up
clubs = Club.where.not( social_media: nil )
total_records = clubs.count
say "Updating # total_records records."
clubs.each.with_index( 1 ) do |club, index|
say "Updating # index of # total_records ...", true
club.social_media = club.social_media.to_h
club.social_media_will_change!
club.save
end
end
def down
puts "Cannot be reverse! See backup table."
end
end
如果您有多个列需要转换,可以轻松修改此迁移以转换所有必要的表和列。
根据您执行此操作的时间,您的数据应该已准备好用于 Rails 5。
【讨论】:
【参考方案3】:@schor 的回复是救命稻草,但我在执行 YAML.load(value) 时不断收到 no implicit conversion of nil into String
错误。
对我有用的是:
class Foo < ApplicationRecord
class NewSerializer
def self.load(value)
return if !value #### THIS NEW LINE
obj = YAML.load(value)
if obj.respond_to?(:to_unsafe_h)
obj.to_unsafe_h
else
obj
end
end
def self.dump(value)
if value.respond_to?(:to_unsafe_h)
YAML.dump(value.to_unsafe_h)
else
YAML.dump(value)
end
end
end
serialize :some_hash_field, NewSerializer
end
我必须管理 Rails 团队,这让我完全措手不及,这是一个最不受欢迎的重大更改,甚至不允许应用获取“旧”数据。
【讨论】:
【参考方案4】:来自fine manual:
序列化(attr_name, class_name_or_coder = Object)
[...] 如果指定了
class_name
,则序列化对象在分配和检索时必须属于该类。否则SerializationTypeMismatch
将被提升。
所以当你这样说时:
serialize :social_media, Hash
ActiveRecord 将要求未序列化的 social_media
为 Hash
。但是,正如vnbrs 所指出的,ActionController::Parameters
不再像以前那样子类化Hash
,并且您有一个充满序列化ActionController::Parameters
实例的表。如果您查看 social_media
列中的原始 YAML 数据,您会看到一堆字符串,例如:
--- !ruby/object:ActionController::Parameters...
而不是像这样的哈希:
---\n:key: value...
您应该修复所有现有数据以在 social_media
中使用 YAML 化哈希,而不是在 ActionController::Parameters
以及其中的任何其他内容。这个过程会有些不愉快:
-
将每个
social_media
作为字符串从表中拉出。
将该 YAML 字符串解压缩为 Ruby 对象:obj = YAML.load(str)
。
将该对象转换为哈希:h = obj.to_unsafe_h
。
将该哈希写回 YAML 字符串:str = h.to_yaml
。
将该字符串放回数据库中以替换 (1) 中的旧字符串。
注意(3) 中的to_unsafe_h
调用。只需在ActionController::Parameters
实例上调用to_h
(或to_hash
)就会在Rails5 中给您一个异常,您必须首先包含permit
调用来过滤参数:
h = params.to_h # Exception!
h = params.permit(:whatever).to_h # Indifferent access hash with one entry
如果您使用to_unsafe_h
(或to_unsafe_hash
),那么您将在HashWithIndifferentAccess
中获得全部内容。当然,如果你真的想要一个普通的旧哈希,那么你会说:
h = obj.to_unsafe_h.to_h
也可以打开无关紧要的访问包装器。这还假设您在social_media
中只有ActionController::Parameters
,因此您可能需要包含obj.respond_to?(:to_unsafe_hash)
检查以查看您如何解压缩social_media
值。
您可以通过 Rails 迁移中的直接数据库访问来进行上述数据迁移。这可能真的很麻烦,具体取决于低级 MySQL 接口的好坏。或者,您可以在迁移中创建一个简化的模型类,如下所示:
class YourMigration < ...
class ModelHack < ApplicationRecord
self.table_name = 'clubs'
serialize :social_media
end
def up
ModelHack.all.each do |m|
# Update this to match your real data and what you want `h` to be.
h = m.social_media.to_unsafe_h.to_h
m.social_media = h
m.save!
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
当然,如果您有很多 Club
s,您会想使用 find_in_batches
或 in_batches_of
而不是 all
。
如果您的 MySQL 支持 json
列并且 ActiveRecord 与 MySQL 的 json
列一起使用(抱歉,这里是 PostgreSQL 人),那么这可能是将列更改为 json
并远离 @987654365 的好时机@。
【讨论】:
【参考方案5】:official Ruby on Rails documentation 有一个关于 Rails 版本之间升级的部分,详细解释了您遇到的错误:
ActionController::Parameters
不再继承HashWithIndifferentAccess
在您的应用程序中调用 params 现在将返回一个对象而不是哈希。如果您的参数已经被允许,那么您将不需要进行任何更改。如果您不考虑permitted?
,则需要将您的应用程序升级为首先允许,然后再转换为哈希。params.permit([:proceed_to, :return_to]).to_h
【讨论】:
谢谢。不过我还是有点困惑。我没有调用params
,而是club.social_media['facebook']
,这是导致错误的原因,因为social_media
不是哈希,而是ActionController::Parameter 对象。以上是关于升级到 Rails 5 时,序列化为哈希的现有数据会产生错误的主要内容,如果未能解决你的问题,请参考以下文章
如何将AngularJS $资源中的params序列化为rails 3.2兼容的参数