升级到 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_mediaHash。但是,正如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

当然,如果您有很多 Clubs,您会想使用 find_in_batchesin_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兼容的参数

现有文本字段的 Rails 操作文本

Rails - 使用 camelize 将模型序列化为 JSON

将 JSON 反序列化为现有对象 (Java)

#=升级到Rails 5后无法正常工作

升级到Rails 5.2.2时的铁路兼容性问题