【问题标题】:Is it possible to have 2 Models loaded for one entity kind to support data migration?是否可以为一种实体类型加载 2 个模型以支持数据迁移?
【发布时间】:2012-01-30 21:09:30
【问题描述】:

我正在为我们当前的生产 App Engine 应用程序编写数据存储迁移。

我们对数据模型进行了一些相当广泛的更改,因此我正在尝试建立一个架构,以便将来更轻松地迁移。这包括迁移测试套件和迁移脚本的通用类结构。

我目前的策略遇到了问题。对于迁移和测试脚本,我需要一种方法将旧模式中的模型类和新数据模式的模型类同时加载到内存中,并使用其中任何一种加载实体。

这是一组示例模式。

rev1.py

class Account(db.Model):
  _version      = db.IntegerProperty(default = 1)
  user          = db.UserProperty(auto_current_user_add = True, required = True)
  name          = db.StringProperty()
  contact_email = db.EmailProperty()

rev2.py

class Account(db.Model):
  _version = db.IntegerProperty(default = 2)
  auth_id  = db.StringProperty()
  name     = db.StringProperty()
  pwd_hash = db.StringProperty(required = True, indexed = False)

迁移脚本可能类似于:

import rev1
import rev2

class MyMigration(...):
   def isNeeded(self):
      num_accounts = num_entities_with_version(rev1.Account, 1)
      return num_accounts > 0

   def run(self):
       rev1_accounts = rev1.Account.all()
       for account in [a for a in rev1_accounts if account._version == 1]:
           auth_id = account.contact_email
           if auth_id is None or auth_id == '':
              auth_id = account.user.email()

              new_account = rev2.Account.create(auth_id = auth_id,
                                                name    = account.name)

测试套件看起来像这样:

import rev1
import rev2

class MyTest(...):
   def testIt(self):
      # Setup data
      act1 = rev1.Account(name = '..', contact_email = '..')
      act1.put()
      act2 = rev1.Account(name = '..', contact_email = '..')
      act2.put()

      # Run migration
      migration.run()

      # Check results
      accounts = rev2.Account.all().fetch(99)

如您所见,我以两种方式使用旧版本。我在迁移中使用它来读取旧格式的数据并将其转换为新格式。 (注意:由于所需的 pwd_hash 字段和其他字段更改等原因,我无法以新格式阅读它)。我在测试套件中使用它在运行迁移之前以旧格式设置测试数据。

这一切在理论上看起来都很好,但在实践中它就崩溃了,因为 GAE 不允许为同一种类型加载多个模型,或者更具体地说,查询只返回最近定义的模型。

在开发服务器中,这似乎是由于在查询实体(例如:Account.get(my_key))时调用 get() 的过程调用了一个结果挂钩,该挂钩通过以下方式构建结果模型对象从数据中调用实体种类名称的 class_for_kind。因此,即使我可以调用 rev2.Account.get(),它也可能会构建 rev1.Account 模型对象,因为类型 'Account' 映射到 _kind_map 字典中的 rev1.Account。

这让我重新考虑了我的迁移策略,我想问问是否有人有想法。具体来说:

  1. 在运行时在测试和生产服务器上手动覆盖 google.appengine.ext.db._kind_map 以允许此迁移方法工作是否安全?
  2. 有没有更好的方法可以同时在内存中保存模型的两个版本?
  3. 是否有其他迁移方法可能是完成这项工作的更明智的方法?

我想过尝试的其他方法包括:

  • 版本更改时更改实体种类。 (使用 kind() 更改它)然后当我们迁移时,我们将所有类移动到新的种类名称。
  • 找到一种方法来查询实体并取回尚未构建到完整对象中的“原始”对象(原型缓冲区??)。 (不适用于测试)
  • “Just Do It Live”:不要为此编写测试,只需尝试使用最新架构进行迁移,加载旧数据以解决出现的问题。

【问题讨论】:

    标签: google-app-engine google-cloud-datastore data-migration


    【解决方案1】:

    我认为在更大的问题中实际上有几个问题。不过这里似乎有两个关键问题,一个是如何测试,另一个是如何真正做到。

    我不会多次定义种类;正如您已经注意到的那样,这样做存在细微差别,而且,如果您最终加载了错误的模型,您会遇到各种各样的麻烦。也就是说,您完全可以操纵 kind_map。我在一些特殊情况下会这样做,但我会尽可能避免这样做。

    对于有重大架构更改的实时迁移,您有两种选择:使用Expando 或使用lower level API。添加必填字段时,您可能会发现使用 Expando 更容易,然后运行迁移以添加新信息,然后切换回普通的 db.Model。较低级别的 API 位于 ext.db 内容的正下方,它将实体呈现为 Python dict。这对于操作实体非常方便。使用您更喜欢的任何方法。如果可能的话,我更喜欢 Expando,因为它是一个更高级别的接口,但它是一个两步过程。

    对于测试,我个人建议您关注实际的转换例程。因此,不要从查询的角度测试方法,而是测试以确保您的转换例程本身正常运行。您甚至可以选择将旧实体作为 Python dict 传入,然后返回新实体。

    我也会在这里进行另一项调整。我宁愿使用查询来查找我所有的 rev 1 帐户。这是在模型上拥有索引 _version 的好处。您可以轻松找到需要迁移的内容。

    另外,请查看 Google 的 article on updating schemas。旧了,但还不错。

    【讨论】:

    • 关于在架构发生重大变化时创建新类型的想法的任何想法。看起来这将允许类似于使用 Expando 的操作,但允许一次完成迁移。 (即为 rev1.Account (kind:acct_v1) 加载所有旧实体,然后将它们保存到新模型 rev2.Account (kind:acct_v2) )。这对我来说似乎更干净一些,然后暂时引入 Expando,然后将其拉回。它可能具有允许测试整个过程的额外好处。毫无疑问,我错过了一些东西。
    • 我得说我喜欢改变种类来指示版本的想法。那可以很干净。如果这把枪太大了,我会走临时的 Expando 路线。您还可以暂时从新字段中删除“必需”标志,然后您可以就地更改实体,您仍然有一些类型安全性。但是请注意,您不能以这种方式完全摆脱旧属性——这需要暂时使用 Expando。但是,您可以在将所有实体转换为新模式后离线运行的一次性 map/reduce 作业中执行该清理步骤。不要弄乱w。实物图。
    • 我认为在两种情况下更新类型以指示版本可能很好:1)真正主要的架构更改和 2)当您没有对该模型的大量引用时(或它们都是键名/id)。如果你有很多交叉引用,你可能只会弄得一团糟。
    • @GuidovanRossum 我同意改变种类可能是干净的。很高兴看到我不是唯一一个。我将研究这个和 expando 方法。
    • @RobertKluin 同意交叉引用。重新连接所有东西可能会很痛苦,因为我所有的交叉引用都是按键。我必须去改变他们改变的每一个地方。我认为某种类型的混合解决方案可能是我最好的选择。
    【解决方案2】:

    另一种方法是简单地在版本 2 上进行迁移,将旧属性保留在模型上,并在更新版本后将它们设置为无。这将清除它们使用的空间,但仍将它们定义。然后在下一个版本中,您可以将它们从模型中删除。

    此方法非常简单,但确实需要两个版本才能完全删除旧属性,因此更类似于弃用现有属性。

    【讨论】:

    • 添加必填字段时不会失败吗?至少在我的测试用例中似乎带有新的 password_hash 字段。我可能会这样做,所以第一次引入该属性时它不是必需的,然后在迁移所有内容后在以下版本中使其成为必需,但我试图避免多次转换过程。
    • 我想说不要立即将该字段设为必填项。在您的业务逻辑中检查它(至少现在是这样)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-22
    • 2020-09-03
    • 2021-04-03
    • 2013-11-06
    相关资源
    最近更新 更多