【开源库学习】libodb库学习(十二)

13 数据库架构演变

  • 当我们添加新的持久类或更改现有的持久类时,例如,通过添加或删除数据成员,存储新对象模型所需的数据库模式也会发生变化。同时,我们可能有包含现有数据的现有数据库。如果应用程序的新版本不需要处理旧数据库,那么模式创建功能就是您所需要的。然而,大多数应用程序都需要使用同一应用程序旧版本存储的数据。

  • 我们将称数据库模式演化为更新数据库以匹配对象模型中的更改的总体任务。模式演化通常由两个子任务组成:模式迁移和数据迁移。架构迁移会修改数据库架构,使其与当前对象模型相对应。例如,在关系数据库中,这可能需要添加或删除表和列。数据迁移任务涉及将存储在现有数据库中的数据从旧格式转换为新格式。

  • 如果手动执行,数据库模式演变是一项乏味且容易出错的任务。因此,ODB为自动化或更准确地说是半自动化的模式演化提供了全面的支持。具体来说,ODB执行全自动模式迁移,并提供工具来帮助您进行数据迁移。

  • 模式演化是一个复杂而敏感的问题,因为通常会有有有价值的生产数据处于危险之中。因此,ODB采取的方法是提供我们可以理解和信任的简单且防弹的基本构建块(或迁移步骤)。使用这些基本块,我们可以实现更复杂的迁移场景。特别是,ODB并不试图自动处理数据迁移,因为在大多数情况下,这需要理解特定于应用程序的语义。换句话说,没有魔法。

  • 有两种处理旧数据的一般方法:应用程序可以将其转换为与新格式相对应的格式,也可以使其能够处理此格式的多个版本。还有一种混合方法,应用程序可以将数据逐步转换为新格式,作为其正常功能的一部分。ODB能够处理所有这些情况。也就是说,支持在不执行任何迁移(模式或数据)的情况下使用旧模型。或者,我们可以迁移模式,之后我们可以选择立即迁移数据(立即数据迁移)或逐步迁移数据(逐步数据迁移)。

  • 模式演化已经是一项复杂的任务,我们不应该不必要地使用更复杂的方法,因为更简单的方法就足够了。综上所述,最简单的方法是不需要任何数据迁移的即时模式迁移。这种更改的一个例子是添加一个具有默认值的新数据成员(第14.3.4节,“默认”)。ODB可以完全自动处理这种情况。

  • 如果我们确实需要数据迁移,那么下一个最简单的方法是立即进行模式和数据迁移。在这里,我们必须编写自定义迁移代码。然而,它与核心应用程序逻辑的其余部分是分开的,并在一个定义良好的点(数据库迁移)执行。换句话说,核心应用程序逻辑不需要知道旧的模型版本。这种方法的潜在缺点是性能。提前转换所有数据可能需要大量资源和/或时间。

  • 如果无法立即迁移,那么下一个选项是立即进行模式迁移,然后进行逐步的数据迁移。通过这种方法,新旧数据必须在新数据库中共存。我们还必须更改应用程序逻辑,以考虑相同数据的不同来源(例如,加载对象的旧版本或新版本时),并在适当的时候迁移数据(例如,更新对象的旧版时)。在某些时候,通常是在大部分数据转换完毕后,逐步迁移会终止,立即迁移。

  • 最复杂的方法是使用多个版本的数据库,而不执行任何迁移、模式或数据。ODB确实为实现这种方法提供了支持(第13.4节,“软对象模型更改”),但我们将在本章中不再进一步介绍。通常,这将需要将关于每个版本的知识嵌入到核心应用程序逻辑中,这使得很难维护任何非平凡的对象模型。

  • 另请注意,在数据迁移方面,我们可以对某些更改使用即时变体,对其他更改使用渐进变体。我们将在第13.3节“数据迁移”中更详细地讨论各种迁移场景。

13.1 对象模型版本和变更日志

  • 为了在ODB中启用模式演化支持,我们需要指定对象模型版本,或者更确切地说,指定两个版本。第一个是基础模型版本。这是我们能够迁移的最低版本。第二个版本是当前的模型版本。在ODB中,我们可以通过从一个版本连续迁移到下一个版本来从多个先前版本迁移,直到我们到达当前版本。我们使用 db model version pragma来指定基本版本和当前版本。

  • 当我们第一次启用模式演化时,我们的基础版本和当前版本将是相同的,例如:

#pragma db model version(1, 1)
  • 一旦我们发布了应用程序,它的用户就可以使用与此版本的对象模型对应的模式创建数据库。这意味着,如果我们对对象模型进行任何修改,也会改变模式,那么我们需要能够将旧数据库迁移到这个新模式。因此,在发布后进行任何新更改之前,我们会递增当前版本,例如:
#pragma db model version(1, 2)
  • 换句话说,我们可以在开发过程中保持相同的版本,并不断向其添加新的更改。但是一旦我们发布了它,对对象模型的任何新更改都必须在新版本中完成。

  • 在对对象模型进行新的更改之前,很容易忘记增加版本。为了帮助解决这个问题,db model version pragma接受第三个可选参数,该参数指定当前版本是打开还是关闭以进行更改。例如:

#pragma db model version(1, 2, open)   // Can add new changes to
                                       // version 2.
  

#pragma db model version(1, 2, closed) // Can no longer add new
                                       // changes to version 2.
  • 如果当前版本已关闭,ODB将拒绝接受任何新的架构更改。在这种情况下,您通常会递增当前版本并将其标记为打开,或者如果需要修复某些问题,您可以重新打开现有版本。但是请注意,重新打开已发布的版本很可能会导致迁移故障。默认情况下,版本是打开的。

  • 通常,应用程序将具有一系列旧数据库版本,可以从中迁移。当我们通过删除对旧版本的支持来更改此范围时,我们还需要调整基本模型版本。这将确保ODB不会保留不必要的信息。

  • 模型版本(包括基本版本和当前版本)是一个64位无符号整数(无符号长整型)。0保留用于表示特殊情况,例如数据库中缺少架构。除此之外,我们可以使用任何值作为版本,只要它们是单调递增的。特别是,我们不必从版本1开始,可以以任何增量增加版本。

  • 一种版本控制方法是使用独立的对象模型版本,从版本1开始并递增1。另一种方法是使模型版本与应用程序版本相对应。例如,如果我们的应用程序使用X.Y.Z版本格式,那么我们可以将其编码为十六进制数,并将其用作我们的模型版本,例如:

#pragma db model version(0x020000, 0x020306) // 2.0.0-2.3.6
  • 大多数真实世界的对象模型将分布在多个头文件中,在每个头文件中重复 db model version pragma将是一项繁重的工作。处理这种情况的推荐方法是将version pragma放入单独的头文件中,并将其包含在对象模型文件中。如果你的项目已经有一个定义应用程序版本的头文件,那么将这个语法放在那里是很自然的。例如:
// version.hxx
//
// Define the application version.
//

#define MYAPP_VERSION 0x020306 // 2.3.6

#ifdef ODB_COMPILER
#pragma db model version(1, 7)
#endif
  • 请注意,我们还可以在version pragma中使用宏,它允许我们在一个地方指定所有版本。例如:
#define MYAPP_VERSION      0x020306 // 2.3.6
#define MYAPP_BASE_VERSION 0x020000 // 2.0.0

#ifdef ODB_COMPILER
#pragma db model version(MYAPP_BASE_VERSION, MYAPP_VERSION)
#endif
  • 同一应用程序中也可能有多个版本不同的对象模型。这些模型必须是独立的,也就是说,一个模型的标题不应包含另一个模型中的标题。您还需要使用--schema-name ODB编译器选项为每个模型分配不同的模式名称。

  • 一旦我们指定了对象模型版本,ODB编译器就会开始在变更日志文件中跟踪数据库模式的更改。变更日志具有基于XML的、面向行的格式。它使用XML来提供人类可读性,同时如果需要,还可以使用自定义工具进行处理和分析。线条方向使得使用diff等工具进行查看变得容易。

  • 变更日志由ODB编译器维护。具体来说,您不需要对此文件进行任何手动更改。然而,从一次调用ODB编译器到下一次调用,您都需要保持它。换句话说,changelog文件既是ODB编译器的输入,也是输出。例如,这意味着如果您的项目的源代码存储在版本控制存储库中,那么您很可能也希望将更改日志存储在那里。如果删除更改日志,则将失去进行架构迁移的任何能力。

  • 您可能希望对更改日志执行的唯一操作是查看因C++对象模型更改而导致的数据库架构更改。为此,您可以使用diff等工具,或者更好的是,您的修订控制系统提供的变更审查功能。为此,变更日志的内容将不言自明。

  • 例如,考虑以下初始对象模型:

// person.hxx
//

#include <string>

#pragma db model version(1, 1)

#pragma db object
class person
{
  ...

  #pragma db id auto
  unsigned long id_;

  std::string first_;
  std::string last_;
};
  • 然后我们用ODB编译器编译这个头文件(以PostgreSQL数据库为例):
odb --database pgsql --generate-schema person.hxx
  • 如果我们现在查看生成的文件列表,那么除了现在熟悉的odb之外。person-odb.?xxperson.sql,我们还将看到person.xml——变更日志文件。以下是此更新日志的内容,仅供说明。
<changelog database="pgsql">
  <model version="1">
    <table name="person" kind="object">
      <column name="id" type="BIGINT" null="false"/>
      <column name="first" type="TEXT" null="false"/>
      <column name="last" type="TEXT" null="false"/>
      <primary-key auto="true">
        <column name="id"/>
      </primary-key>
    </table>
  </model>
</changelog>
  • 假设我们现在想向person类添加另一个数据成员——中间名。我们递增版本并进行更改:
#pragma db model version(1, 2)

#pragma db object
class person
{
  ...

  #pragma db id auto
  unsigned long id_;

  std::string first_;
  std::string middle_;
  std::string last_;
};
  • 我们使用完全相同的命令行重新编译我们的文件:
odb --database pgsql --generate-schema person.hxx
  • 这一次,ODB编译器将读取旧的更改日志,更新它,并写出新版本。同样,仅供说明,以下是更新的变更日志内容:
<changelog database="pgsql">
  <changeset version="2">
    <alter-table name="person">
      <add-column name="middle" type="TEXT" null="false"/>
    </alter-table>
  </changeset>

  <model version="1">
    <table name="person" kind="object">
      <column name="id" type="BIGINT" null="false"/>
      <column name="first" type="TEXT" null="false"/>
      <column name="last" type="TEXT" null="false"/>
      <primary-key auto="true">
        <column name="id"/>
      </primary-key>
    </table>
  </model>
</changelog>
  • 只是重申一下,虽然更改日志可能看起来像是手工编写的,但它完全由ODB编译器自动维护,您可能想查看其内容的唯一原因是查看数据库模式更改。例如,如果我们将上述两个更改日志与diff进行比较,我们将得到以下数据库模式更改的摘要:
--- person.xml.orig
+++ person.xml
@@ -1,4 +1,10 @@
<changelog database="pgsql">
+  <changeset version="2">
+    <alter-table name="person">
+      <add-column name="middle" type="TEXT" null="false"/>
+    </alter-table>
+  </changeset>
+
  <model version="1">
    <table name="person" kind="object">
      <column name="id" type="BIGINT" null="false"/>
  • 仅当我们生成数据库模式时,即指定了--generate-schema选项时,才会写入更改日志。只生成数据库支持代码(C++)的ODB编译器的调用不会读取或更新更改日志。换句话说,changelog跟踪的是结果数据库模式中的更改,而不是C++对象模型。

ODB在比较数据库模式时忽略列顺序。这意味着我们可以重新排序类中的数据成员,而不会导致任何模式更改。但是,成员重命名将导致架构更改,因为列名也会更改(除非我们明确指定列名)。从ODB的角度来看,这样的重命名看起来像是删除一个数据成员并添加另一个。如果我们不希望这被视为模式更改,那么我们需要通过 db column pragma显式指定来保留旧的列名。例如,我们可以将middle_重命名为middle_name_,而不会导致任何模式更改:

#pragma db model version(1, 2)

#pragma db object
class person
{
  ...

  #pragma db column("middle") // Keep the original column name.
  std::string middle_name_;

  ...
};
  • 如果您的对象模型由大量头文件组成,并且您为每个头文件单独生成了数据库模式,那么将为每个头档案创建一个变更日志。这可能是你想要的,但是,大量的变更日志很快就会变得难以处理。事实上,如果您将数据库模式作为独立的SQL文件生成,那么您可能已经遇到了由大量.sql文件(每个头对应一个)引起的类似问题。

  • 这两个问题的解决方案是生成一个组合的数据库模式文件和一个变更日志。例如,假设我们的对象模型中有三个头文件:person.hxxemployee.hxxemployee.hxx。为了生成数据库支持代码,我们像往常一样编译它们,但不指定--generate-schema选项。在这种情况下,不会创建或更新更改日志:

odb --database pgsql person.hxx
odb --database pgsql employee.hxx
odb --database pgsql employer.hxx
  • 为了生成数据库模式,我们单独调用ODB编译器。然而,这一次,我们指示它只生成模式(--generate-schema only),并为对象模型中的所有文件同时生成模式:
odb --database pgsql --generate-schema-only --at-once \
--input-name company person.hxx employee.hxx employer.hxx
  • 上述命令的结果是一个company.sql文件(名称来自--input-name值),其中包含整个对象模型的数据库模式。还有一个相应的变更日志文件--company.xml

通过指示ODB编译器将数据库创建代码生成到单独的C++文件中(--schema format separate),嵌入式模式也可以实现同样的效果:

odb --database pgsql --generate-schema-only --schema-format separate \
--at-once --input-name company person.hxx employee.hxx employer.hxx
  • 此命令的结果是一个company-schema.xxx文件,以及company.xml文件。

  • 另请注意,默认情况下,更改日志文件不会放置在使用--output-dir选项指定的目录中。这是因为更改日志同时是输入和输出文件。因此,默认情况下,ODB编译器会将其放置在输入头文件的目录中。

  • 然而,有许多命令行选项(包括--changelog-dir)允许我们微调changelog文件的名称和位置。例如,您可以指示ODB编译器从一个文件读取更改日志,同时将其写入另一个文件。例如,如果您想在丢弃旧文件之前查看更改,这可能很有用。有关这些选项的更多信息,请参阅《ODB编译器命令行手册》并搜索“changelog”。

  • 当我们在上面讨论版本增量时,我们使用了开发和发布这两个术语。具体来说,我们讨论了在开发期间保持相同的对象模型版本,并在发布后对其进行增量。在这种情况下,什么是开发期和发布期?这些定义可能因项目而异。通常,在开发期间,我们会对对象模型进行一个或多个更改,这些更改会导致数据库模式的更改。发布是指我们将更改提供给可能有旧数据库要迁移的其他人。在传统意义上,发布是指向用户提供应用程序的新版本。然而,出于模式演化的目的,发布也可能意味着简单地将更改模式的更改提供给团队中的其他开发人员。让我们考虑两种常见的情况来说明这一切是如何结合在一起的。

  • 设置项目的一种方法是重复使用应用程序开发期和应用程序版本进行模式演化。也就是说,在新的应用程序版本开发过程中,我们保持一个单一的对象模型版本,当我们发布应用程序时,我们会增加模型版本。在这种情况下,为了保持一致性,将应用程序版本重用为模型版本也是有意义的。以下是此设置的分步指南:

  1. 在开发过程中,保持当前对象模型版本打开。
  2. 在发布之前(例如,当输入“功能冻结”时)关闭版本。
  3. 发布后,更新版本并打开它。
  4. 对于每个新功能,请查看变更日志顶部的变更集,例如,使用diff或版本控制工具。如果您正在使用版本控制,那么最好在将更改提交到存储库之前完成。
  • 在项目中设置模式版本控制的另一种方法是将开发期定义为开发单个功能,并将版本定义为将此功能提供给团队中的其他人(开发人员、测试人员等),例如,通过将更改提交到公共版本控制存储库。在这种情况下,对象模型版本将独立于应用程序版本,可以简单地是一个从1开始并递增1的序列。以下是此设置的分步指南:
  1. 保持当前模型版本关闭。一旦做出影响数据库模式的更改,ODB编译器将拒绝更新更改日志。
  2. 如果更改是合法的,请打开一个新版本,即递增当前版本并使其打开。
  3. 一旦实现并测试了该功能,请查看最终的数据库更改集(使用diff或您的版本控制工具),关闭版本,并将更改提交到版本控制存储库(如果使用)。
  • 如果您使用的版本控制存储库支持预提交检查,那么您可能需要考虑添加这样的检查,以确保提交的版本始终关闭。

  • 如果我们刚刚开始在项目中进行模式演化,我们应该选择哪种方法?这两种方法在不同的情况下效果更好,因为它们有不同的优缺点。第一种方法,我们可以称之为每个应用程序版本,最适合具有较小版本的简单项目,因为否则一次迁移将捆绑大量与不同功能相对应的无关操作。这可能会变得难以审查,如果出现问题,可以进行调试。

  • 第二种方法,我们可以称之为每个功能的版本,它更加模块化,并提供了许多额外的好处。我们可以为每个功能执行迁移,这是一个谨慎的步骤,使调试更容易。我们还可以将每个这样的迁移步骤放入单独的事务中,进一步提高可靠性。它在更大的团队中也可以更好地扩展,在这些团队中,多个开发人员可以同时处理影响数据库模式的功能。例如,如果你发现你的团队中的另一个开发人员使用了与你相同的版本,并设法在你之前提交了他的更改(也就是说,你有合并冲突),那么你可以简单地将版本更改为下一个可用版本,重新生成更改日志,然后继续提交。

  • 总的来说,除非你有充分的理由更喜欢按应用程序发布版本的方法,否则即使一开始看起来更复杂,也要选择按功能发布版本。此外,如果您确实选择了第一种方法,请考虑通过保留子版本号来配置切换到第二种方法。例如,对于2.3.4格式的应用程序版本,可以将对象模型版本设置为0x0203040000格式,将最后两个字节保留为子版本。稍后,您可以使用它切换到每个功能的版本方法。

13.2 架构迁移

  • 一旦我们通过指定对象模型版本启用模式演化,除了模式创建语句外,ODB编译器还开始为每个版本从基础到当前生成模式迁移语句。与模式创建一样,模式迁移可以作为一组SQL文件生成,也可以嵌入到生成的C++代码中(--schema-format选项)。

  • 对于每个迁移步骤,即从一个版本到下一个版本,ODB生成两组语句:迁移前和迁移后。迁移前语句“放宽”了数据库模式,以便新旧数据可以共存。在这个阶段,添加新的列和表,同时删除旧的约束。迁移后的语句将数据库模式“收紧”,以便只保留符合新格式的数据。在此阶段,旧的列和表将被删除,新的约束将被添加。现在,您可能可以猜测数据迁移在哪里适合这一点——在模式前和模式后迁移之间,我们都可以访问旧数据并创建新数据。

  • 如果模式是作为独立的SQL文件生成的,那么我们最终会为每个步骤生成一对文件:迁移前文件和迁移后文件。对于我们在上一节中开始的人员示例,我们将有person-002-pre.sqlperson-002-post.sql文件。这里002是我们要迁移到的版本,而前缀和后缀指定了迁移阶段。因此,如果我们想将人员数据库从版本1迁移到版本2,那么我们将首先执行person-002-pre.sql,然后迁移数据(如果有的话)(下一节将更详细地讨论),最后执行person-002-1ost.sql。如果我们的数据库落后于多个版本,例如数据库的版本为1,而当前版本为5,那么我们只需对每个版本执行这组步骤,直到我们达到当前版本。

  • 如果我们查看person-002-pre.sql文件的内容,我们将看到以下语句(或等效语句,具体取决于所使用的数据库):

ALTER TABLE "person"
  ADD COLUMN "middle" TEXT NULL;
  • 正如我们所料,此语句添加了一个与新数据成员对应的新列。然而,细心的读者会注意到,即使我们从未在对象模型中请求过这种语义,该列也被添加为NULL。为什么该列被添加为NULL?如果在迁移过程中,person表已经包含行(即现有对象),则尝试添加没有默认值的非NULL列将失败。因此,ODB最初会添加一个没有默认值为NULL的新列,但会在迁移后阶段对其进行清理。这样,您的数据迁移代码就有机会为所有现有对象的新数据成员分配一些有意义的值。以下是person-002-post.sql文件的内容:
ALTER TABLE "person"
  ALTER COLUMN "middle" SET NOT NULL;
  • 目前,ODB直接支持以下基本数据库模式更改:
  • 添加表格
  • 删除表格
  • 添加列
  • 删除列
  • 更改列,设置NULL/NOT NULL
  • 添加外键
  • 删除外键
  • 添加索引
  • 删除索引
  • 通常可以根据这些构建块实施更复杂的更改。例如,要更改数据成员的类型(这会导致列类型的更改),我们可以添加具有所需类型的新数据成员(添加列),迁移数据,然后删除旧数据成员(删除列)。ODB将对目前不直接支持的情况进行诊断。还要注意,一些数据库系统(特别是SQLite)在支持模式更改方面存在许多限制。有关这些数据库特定限制的更多信息,请参阅第二部分“数据库系统”中的“限制”部分。

  • 我们如何知道当前的数据库版本是什么?也就是说,我们需要从哪个版本迁移?例如,我们需要知道这一点,以便确定我们必须执行的迁移集。默认情况下,当启用模式演化时,ODB将此信息保存在一个名为schema_version的特殊表中,该表具有以下(或等效的,取决于所使用的数据库)定义:

CREATE TABLE "schema_version" (
  "name" TEXT NOT NULL PRIMARY KEY,
  "version" BIGINT NOT NULL,
  "migration" BOOLEAN NOT NULL);
  • name列是使用--schema-name选项指定的架构名称。默认架构为空。版本列包含当前数据库版本。最后,迁移标志指示我们是否正在迁移数据库,即在迁移前和迁移后阶段之间。

  • 模式创建语句(在我们的例子中为person.sql)创建此表,并用初始模型版本填充它。例如,如果我们执行了与对象模型版本1对应的person.sql,那么name将为空(这表示默认模式,因为我们没有指定--schema-name),version将为1,迁移将为FALSE。

  • 迁移前语句更新版本并将迁移标志设置为TRUE。继续我们的示例,执行person-002-pre.sql后,版本将变为2,迁移将设置为TRUE。迁移后声明只是清除了迁移标志。在我们的例子中,运行person-002-post.sql后,版本将保持为2,而迁移将重置为FALSE。

  • 还要注意,上面我们提到了模式创建语句(person.sql)创建schema_version表。这意味着,如果我们在项目中期启用模式进化支持,那么我们可能已经有了不包括该表的现有数据库。因此,除非我们手动添加schema_version表并用正确的版本信息填充它,否则ODB将无法处理此类数据库的迁移。因此,强烈建议您考虑是否使用模式演化,如果是,请从项目开始就启用它。

  • odb::database类提供了一个API,用于访问和修改当前数据库版本:

namespace odb
{
  typedef unsigned long long schema_version;

  struct LIBODB_EXPORT schema_version_migration
  {
    schema_version_migration (schema_version = 0,
                              bool migration = false);

    schema_version version;
    bool migration;

    // This class also provides the ==, !=, <, >, <=, and >= operators.
    // Version ordering is as follows: {1,f} < {2,t} < {2,f} < {3,t}.
  };

  class database
  {
  public:
    ...

    schema_version
    schema_version (const std::string& name = "") const;

    bool
    schema_migration (const std::string& name = "") const;

    const schema_version_migration&
    schema_version_migration (const std::string& name = "") const;

    // Set schema version and migration state manually.
    //
    void
    schema_version_migration (schema_version,
                              bool migration,
                              const std::string& name = "");

    void
    schema_version_migration (const schema_version_migration&,
                              const std::string& name = "");

    // Set default schema version table for all schemas.
    //
    void
    schema_version_table (const std::string& table_name);

    // Set schema version table for a specific schema.
    //
    void
    schema_version_table (const std::string& table_name,
                          const std::string& name);
  };
}
  • schema_version()schema_migration()访问器分别返回当前数据库版本和迁移标志。可选的name参数是架构名称。如果数据库模式尚未创建(即schema_version表中没有相应的条目或此表不存在),则schema_version()返回0。schema_version_migration()访问器在schema_version_migration结构中同时返回版本和迁移标志。

  • 您的数据库中可能已经有一个版本表,或者您(或您的数据库管理员)可能更喜欢以自己的方式跟踪版本。您可以使用--suppress-schema-version选项指示ODB不要创建schema_version表。然而,为了使某些模式演化机制正常工作,ODB仍然需要知道当前的数据库版本。因此,在这种情况下,您需要使用schema_version_migration()修饰符手动设置数据库实例上的模式版本。请注意,修改器API不是线程安全的。也就是说,当其他线程可能正在访问或修改相同的信息时,您不应该修改架构版本。

  • 还要注意,我们上面讨论的访问器只会查询schema_version表一次,如果可以确定版本,则缓存结果。但是,如果无法确定版本(即schema_version()返回0),则后续调用将重新查询该表。虽然在应用程序运行时修改数据库架构(而不是通过schema_catalog API,如下所述)可能不是一个好主意,但如果出于某种原因需要ODB来重新查询版本,则可以使用schema_version_mmigration()修饰符将其手动设置为0。

  • 还可以使用--schema-version-table选项更改存储模式版本的表的名称。您还需要使用schema_version_table()修饰符在数据库实例上指定此替代名称。第一个版本指定了用于所有模式名称的默认表。第二个版本指定了特定模式的表。如有必要,表名应使用数据库引号。

  • 如果我们将模式迁移作为独立的SQL文件生成,那么迁移工作流可能如下:

  1. 数据库管理员确定当前数据库版本。如果需要迁移,那么对于每个迁移步骤(即从一个版本到下一个版本),他都会执行以下操作:
    执行预迁移文件。
  2. 执行我们的应用程序(或单独的迁移程序)以执行数据迁移(稍后讨论)。我们的应用程序可以通过调用schema_migration()来确定正在以“迁移模式”执行,然后通过调用schema_version()来运行哪个迁移代码。
  3. 执行迁移后文件。
  • 如果我们将模式创建和迁移代码嵌入到生成的C++代码中,这些步骤将变得更加集成和自动化。现在,我们可以执行模式创建、模式迁移和数据迁移,并从应用程序中以编程方式确定何时需要执行每个步骤。

  • 模式演化支持为odb::schema_catalog类添加了以下额外函数,我们在第3.4节“数据库”中首次讨论了这些函数。

namespace odb
{
  class schema_catalog
  {
  public:
    ...


    // Schema migration.
    //
    static void
    migrate_schema_pre (database&,
                        schema_version,
                        const std::string& name = "");

    static void
    migrate_schema_post (database&,
                         schema_version,
                         const std::string& name = "");

    static void
    migrate_schema (database&,
                    schema_version,
                    const std::string& name = "");

    // Data migration.
    //
    // Discussed in the next section.


    // Combined schema and data migration.
    //
    static void
    migrate (database&,
             schema_version = 0,
             const std::string& name = "");

    // Schema version information.
    //
    static schema_version
    base_version (const database&,
                  const std::string& name = "");

    static schema_version
    base_version (database_id,
                  const std::string& name = "");

    static schema_version
    current_version (const database&,
                     const std::string& name = "");

    static schema_version
    current_version (database_id,
                     const std::string& name = "");

    static schema_version
    next_version (const database&,
                  schema_version = 0,
                  const std::string& name = "");

    static schema_version
    next_version (database_id,
                  schema_version,
                  const std::string& name = "");
  };
}
  • migrate_schema_pre()migrate_sschema_post()静态函数执行单个迁移步骤的单个阶段(即,从一个版本到下一个版本)。version参数指定了我们要迁移到的版本。例如,在我们的个人示例中,如果我们知道数据库版本为1,下一个版本为2,那么我们可以执行如下代码:
transaction t (db.begin ());

schema_catalog::migrate_schema_pre (db, 2);

// Data migration goes here.

schema_catalog::migrate_schema_post (db, 2);

t.commit ();
  • 如果你没有任何数据迁移代码要运行,那么你可以使用migrate_schema()静态函数通过一次调用来执行这两个阶段。

  • migrate()静态函数执行模式和数据迁移(我们将在下一节讨论数据迁移)。它还可以同时执行多个迁移步骤。如果我们不指定其目标版本,那么它将一直迁移(如果需要)到当前模型版本。为了更加方便,migrate()还将创建数据库模式(如果不存在)。因此,如果我们没有任何数据迁移代码,或者我们已经在schema_catalog中注册了它(如稍后所述),那么数据库模式的创建和迁移(无论是否需要)都可以通过一个函数调用来执行:

transaction t (db.begin ());
schema_catalog::migrate (db);
t.commit ();
  • 还要注意schema_catalogodb::database模式版本API集成。特别是,schema_catalog函数将在需要时查询和同步数据库实例上的模式版本。

  • schema_catalog类还允许您使用base_version()current_version()next_version()静态函数迭代已知版本(记住,版本号中可能存在“间隙”)。base_version()current_version()函数分别返回基本和当前对象模型版本。也就是说,我们可以迁移的最低版本和我们最终想要迁移到的版本。next_version()函数返回下一个已知版本。如果传递的版本大于或等于当前版本,则此函数将返回当前版本加一(即过去的当前版本)。如果我们不指定版本,那么next_version()将使用当前数据库版本作为起点。还要注意,只有当我们将模式迁移代码嵌入到生成的C++代码中时,这些函数提供的模式版本信息才可用。对于独立的SQL文件迁移,通常不需要此信息,因为迁移过程是由外部实体(如数据库管理员或脚本)指导的。

  • 上面介绍的大多数schema_catalog函数也接受可选的模式名称参数。如果找不到传递的模式名称,则抛出odb::unknown_schema异常。同样,如果传递的版本无效,接受模式版本参数的函数将抛出odb::unknown_schema_version异常。有关这些例外的更多信息,请参阅第3.14节“ODB例外”

  • 为了说明所有这些部分是如何组合在一起的,请考虑以下更现实的数据库模式管理示例。在这里,我们希望以一种特殊的方式处理模式创建,并在自己的事务中执行每个迁移步骤。

schema_version v (db.schema_version ());
schema_version bv (schema_catalog::base_version (db));
schema_version cv (schema_catalog::current_version (db));

if (v == 0)
{
  // No schema in the database. Create the schema and
  // initialize the database.
  //
  transaction t (db.begin ());
  schema_catalog::create_schema (db);

  // Populate the database with initial data, if any.

  t.commit ();
}
else if (v < cv)
{
  // Old schema (and data) in the database, migrate them.
  //

  if (v < bv)
  {
    // Error: migration from this version is no longer supported.
  }

  for (v = schema_catalog::next_version (db, v);
       v <= cv;
       v = schema_catalog::next_version (db, v))
  {
    transaction t (db.begin ());
    schema_catalog::migrate_schema_pre (db, v);

    // Data migration goes here.

    schema_catalog::migrate_schema_post (db, v);
    t.commit ();
  }
}
else if (v > cv)
{
  // Error: old application trying to access new database.
}

13.3 数据迁移

  • 在相当多的情况下,为新数据成员指定默认值将是处理现有对象所需的全部内容。例如,我们添加的新中间名的自然默认值是一个空字符串。我们可以使用db default pragma处理这种情况,而无需任何额外的C++代码:
#pragma db model version(1, 2)

#pragma db object
class person
{
  ...


  #pragma db default("")
  std::string middle_;
};
  • 然而,在某些情况下,我们需要执行更复杂的数据迁移,即将旧数据转换为新格式。例如,假设我们想在person类中添加性别。而且,我们将尝试从名字猜测它,而不是对所有现有对象不进行分配。这不是特别准确,但对于我们的假设应用来说可能就足够了:
#pragma db model version(1, 3)

enum gender {male, female};

#pragma db object
class person
{
  ...

  gender gender_;
};
  • 正如我们之前讨论的那样,有两种方法可以执行数据迁移:即时和渐进。简而言之,通过立即迁移,我们一次迁移所有现有对象,通常在模式迁移前语句之后但在迁移后语句之前。通过逐步迁移,我们确保新的对象模型可以容纳新旧数据,并随着应用程序的运行和机会的出现(例如,对象被更新)逐步迁移现有对象。

  • 还有另一种数据迁移选项,本节将不再进一步讨论。我们可以执行即席SQL语句,直接在数据库服务器上执行必要的转换和迁移,而不是使用我们的C++对象模型。虽然在某些情况下,从性能的角度来看,这可能是一个更好的选择,但这种方法在我们可以处理的迁移逻辑方面往往是有限的。

13.3.1 即时数据迁移

  • 首先,让我们看看如何为上面添加的新gender_data成员实现即时迁移。如果我们使用独立的SQL文件进行迁移,那么我们可以在main()的早期,在主应用程序逻辑之前添加以下代码:
int
main ()
{
  ...

  odb::database& db = ...

  // Migrate data if necessary.
  //
  if (db.schema_migration ())
  {
    switch (db.schema_version ())
    {
    case 3:
      {
        // Assign gender to all the existing objects.
        //
        transaction t (db.begin ());

        for (person& p: db.query<person> ())
        {
          p.gender (guess_gender (p.first ()));
          db.update (p);
        }

        t.commit ();
        break;
      }
    }
  }

  ...
}
  • 如果您有大量对象要迁移,从性能的角度来看,将我们现在拥有的一个大事务分解为多个较小的事务也可能是一个好主意(第3.5节,“事务”)。例如:
case 3:
  {
    transaction t (db.begin ());

    size_t n (0);
    for (person& p: db.query<person> ())
    {
      p.gender (guess_gender (p.first ()));
      db.update (p);

      // Commit the current transaction and start a new one after
      // every 100 updates.
      //
      if (n++ % 100 == 0)
      {
        t.commit ();
        t.reset (db.begin ());
      }
    }

    t.commit ();
    break;
  }
  • 虽然它看起来很简单,但随着我们添加更多的迁移片段,这种方法很快就会变得无法维护。我们可以将每个迁移打包到一个单独的函数中,向schema_catalog类注册,并让ODB决定何时运行哪些迁移函数,而不是将所有迁移都放在一个函数中,并自行决定何时运行每个迁移函数。为了支持此功能,schema_catalog提供了以下数据迁移API:
namespace odb
{
  class schema_catalog
  {
  public:
    ...

    // Data migration.
    //
    static std::size_t
    migrate_data (database&,
                  schema_version = 0,
                  const std::string& name = "");

    typedef void data_migration_function_type (database&);

    // Common (for all the databases) data migration, C++98/03 version:
    //
    template <schema_version v, schema_version base>
    static void
    data_migration_function (data_migration_function_type*,
                             const std::string& name = "");

    // Common (for all the databases) data migration, C++11 version:
    //
    template <schema_version v, schema_version base>
    static void
    data_migration_function (std::function<data_migration_function_type>,
                             const std::string& name = "");

    // Database-specific data migration, C++98/03 version:
    //
    template <schema_version v, schema_version base>
    static void
    data_migration_function (database&,
                             data_migration_function_type*,
                             const std::string& name = "");

    template <schema_version v, schema_version base>
    static void
    data_migration_function (database_id,
                             data_migration_function_type*,
                             const std::string& name = "");

    // Database-specific data migration, C++11 version:
    //
    template <schema_version v, schema_version base>
    static void
    data_migration_function (database&,
                             std::function<data_migration_function_type>,
                             const std::string& name = "");

    template <schema_version v, schema_version base>
    static void
    data_migration_function (database_id,
                             std::function<data_migration_function_type>,
                             const std::string& name = "");
  };

  // Static data migration function registration, C++98/03 version:
  //
  template <schema_version v, schema_version base>
  struct data_migration_entry
  {
    data_migration_entry (data_migration_function_type*,
                          const std::string& name = "");

    data_migration_entry (database_id,
                          data_migration_function_type*,
                          const std::string& name = "");
  };

  // Static data migration function registration, C++11 version:
  //
  template <schema_version v, schema_version base>
  struct data_migration_entry
  {
    data_migration_entry (std::function<data_migration_function_type>,
                          const std::string& name = "");

    data_migration_entry (database_id,
                          std::function<data_migration_function_type>,
                          const std::string& name = "");
  };
}
  • migrate_data()静态函数为指定版本执行数据迁移。如果没有指定版本,则它将使用当前数据库版本,并检查数据库是否处于迁移中,即database::schema_migration()返回true。因此,我们只需在main()中调用此函数即可。它将检查是否需要迁移,如果需要,则调用为此版本注册的所有迁移函数。例如:
int
main ()
{
  ...

  database& db = ...

  // Check if we need to migrate any data and do so
  // if that's the case.
  //
  schema_catalog::migrate_data (db);

  ...
}
  • migrate_data()函数返回调用的迁移函数的数量。您可以将此值用于调试或日志记录。

  • 我们需要执行的另一个步骤是在schema_catalog中注册我们的数据迁移函数。在较低级别,我们可以为每个迁移函数调用data_migration_function()静态函数,例如在main()的开头。对于每个版本,数据迁移函数按注册顺序调用。

  • 然而,一种更方便的方法是在静态初始化期间使用data_migration_entry辅助类模板来注册迁移函数。这样我们就可以将迁移功能及其注册码放在一起。以下是我们如何重新实现我们的性别迁移代码以使用此机制:

static void
migrate_gender (odb::database& db)
{
  transaction t (db.begin ());

  for (person& p: db.query<person> ())
  {
    p.gender (guess_gender (p.first ()));
    db.update (p);
  }

  t.commit ();
}

static const odb::data_migration_entry<3, MYAPP_BASE_VERSION>
migrate_gender_entry (&migrate_gender);
  • data_migration_entry类模板的第一个模板参数是我们希望调用此数据迁移函数的版本。第二个模板参数是基本模型版本。第二个参数对于检测我们不再需要此数据迁移功能的情况是必要的。请记住,当我们向前移动基础模型版本时,不再可能从新基础以下的任何版本进行迁移。然而,我们可能仍然为这些较低版本注册了迁移功能。由于这些函数永远不会被调用,因此它们实际上是死代码,识别和删除它们会很有用。为了帮助实现这一点,data_migration_entry(和低级data_migration _function())将在编译时(即static_assent)检查注册版本是否大于基本模型版本。

  • 在上面的示例中,我们使用MYAPP_BASE_VERSION宏,该宏可能在一个中心位置定义,例如version.hxx。这是推荐的方法,因为我们可以在一个地方更新基本版本,并让C++编译器自动识别所有可以删除的数据迁移函数。

  • 在C++11中,我们还可以创建一个模板别名,这样我们就不必在每次注册中重复基本模型宏,例如:

template <schema_version v>
using migration_entry = odb::data_migration_entry<v, MYAPP_BASE_VERSION>;

static const migration_entry<3>
migrate_gender_entry (&migrate_gender);
  • 对于需要绕过基本版本检查的情况,例如,为了实现自己的注册助手,ODB还提供了data_migration_function()函数的“不安全”版本,这些版本将版本作为函数参数而不是模板参数。

  • 在C++11中,我们还可以使用lambdas作为迁移函数,这使得迁移代码更加简洁:

static const migration_entry<3>
migrate_gender_entry (
  [] (odb::database& db)
  {
    transaction t (db.begin ());

    for (person& p: db.query<person> ())
    {
      p.gender (guess_gender (p.first ()));
      db.update (p);
    }

    t.commit ();
  });
  • 如果我们使用嵌入式模式迁移,那么模式和数据迁移都是集成的,可以通过调用我们前面讨论的schema_catalog::migrate()函数来执行。例如:
int
main ()
{
  ...

  database& db = ...

  // Check if we need to migrate the database and do so
  // if that's the case.
  //
  {
    transaction t (db.begin ());
    schema_catalog::migrate (db);
    t.commit ();
  }

  ...
}
  • 但是请注意,在这种情况下,我们在事务中调用migrate()(用于模式迁移部分),这意味着我们的迁移函数也将在此事务中被调用。因此,我们需要调整迁移功能,不要启动自己的事务:
static void
migrate_gender (odb::database& db)
{
  // Assume we are already in a transaction.
  //
  for (person& p: db.query<person> ())
  {
    p.gender (guess_gender (p.first ()));
    db.update (p);
  }
}
  • 然而,如果我们想要更细粒度的事务,那么我们可以使用较低级别的schema_catalog函数来获得更多的控制,正如我们在上一节末尾看到的那样。以下是该示例的相关部分,并添加了数据迁移调用:
  // Old schema (and data) in the database, migrate them.
  //
  for (v = schema_catalog::next_version (db, v);
       v <= cv;
       v = schema_catalog::next_version (db, v))
  {
    transaction t (db.begin ());
    schema_catalog::migrate_schema_pre (db, v);
    schema_catalog::migrate_data (db, v);
    schema_catalog::migrate_schema_post (db, v);
    t.commit ();
  }

13.3.2 逐步数据迁移

  • 如果需要迁移的现有对象数量很大,那么一次性立即迁移虽然简单,但从性能角度来看可能不切实际。在这种情况下,我们可以在应用程序执行正常功能的同时执行逐步迁移。

  • 随着逐步迁移,对象模型必须能够同时表示符合新旧格式的数据,因为一般来说,数据库将包含新旧对象的混合。例如,对于我们的性别数据成员,我们需要一个特殊的值来表示“尚未分配性别”的情况(一个旧对象)。我们还需要在模式预迁移阶段将此特殊值分配给所有现有对象。一种方法是在我们的性别枚举中添加一个特殊值,然后使用db default pragma将其设置为默认值。然而,一种更干净、更简单的方法是使用NULL作为特殊值。我们可以通过用odb::nullableboost::optional或类似的方式包装它,为任何现有类型添加对NULL值语义的支持(第7.3节,“指针和NULL值语义”)。我们也不需要显式指定默认值,因为NULL是自动使用的。以下是我们如何在性别示例中使用这种方法:

#include <odb/nullable.hxx>

#pragma db object
class person
{
  ...

  odb::nullable<gender> gender_;
};
  • 可以采用各种策略来实施逐步迁移。例如,当对象作为正常应用程序逻辑的一部分被更新时,我们可以迁移数据。虽然这种方法没有迁移成本(无论如何都会更新对象),但根据对象通常更新的频率,这种策略可能需要很长时间才能完成。另一种策略是在加载旧对象时执行更新。另一种策略是有一个单独的线程,在应用程序运行时缓慢迁移所有旧对象。

  • 例如,让我们实施gender迁移的第一种方法。虽然我们本可以在整个应用程序中添加必要的代码,但从维护的角度来看,最好尝试将渐进迁移逻辑本地化到它所影响的持久类。对于这个数据库操作,回调(第14.1.7节,“回调”)是一种非常有用的机制。在我们的例子中,我们所要做的就是处理post_load事件,如果它为NULL,我们就猜测性别:

#include <odb/core.hxx>     // odb::database
#include <odb/callback.hxx> // odb::callback_event
#include <odb/nullable.hxx>

#pragma db object callback(migrate)
class person
{
  ...

  void
  migrate (odb::callback_event e, odb::database&)
  {
    if (e == odb::callback_event::post_load)
    {
      // Guess gender if not assigned.
      //
      if (gender_.null ())
        gender_ = guess_gender (first_);
    }
  }

  odb::nullable<gender> gender_;
};
  • 特别是,我们不必触摸任何访问器、修饰符或应用程序逻辑——所有这些都可以假设值永远不会为NULL。当对象下次更新时,新的gender值将自动存储。

  • 当大部分对象可能已经转换时,所有逐步迁移通常最终都会终止一些版本的即时迁移。这样我们就不必永远保留渐进式迁移代码。以下是我们如何为示例实现终止迁移:

// person.hxx
//
#pragma db model version(1, 4)

#pragma db object
class person
{
  ...

  gender gender_;
};

// person.cxx
//
static void
migrate_gender (odb::database& db)
{
  typedef odb::query<person> query;

  for (person& p: db.query<person> (query::gender.is_null ()))
  {
    p.gender (guess_gender (p.first ()));
    db.update (p);
  }
}

static const odb::data_migration_entry<4, MYAPP_BASE_VERSION>
migrate_gender_entry (&migrate_gender);
  • 关于这段代码,有几点需要注意。首先,我们从类中删除了所有渐进迁移逻辑(回调),并将其替换为即时迁移函数。我们还删除了odb::nullable包装器(因此不允许NULL值),因为在此迁移后,所有对象都将被转换。最后,在迁移函数中,我们只查询数据库中需要迁移的对象,即具有NULL性别的对象。

13.4 软对象模型更改

  • 让我们考虑另一种常见的对象模型更改:我们删除一个旧成员,添加一个新成员,并需要将数据从旧成员复制到新成员,也许需要应用一些转换。例如,我们可能会意识到,在我们的应用程序中,最好将一个人的名字存储为单个字符串,而不是将其拆分为三个字段。因此,我们想做的是添加一个新的数据成员,我们称之为name_,转换所有现有的拆分名称,然后删除first_middle_last_数据成员。

  • 虽然这听起来很简单,但有一个问题。如果我们删除(即从源代码中物理删除)旧数据成员,那么我们将无法访问旧数据。在模式迁移前后,数据库中的数据仍然可用,只是我们将无法再通过对象模型访问它。如果我们保留旧数据成员,那么即使在迁移后的模式之后,旧数据也会保留在数据库中。

  • 还有一个更微妙的问题,与以前版本的现有迁移有关。记住,在person示例的版本3中,我们添加了gender_data成员。我们还有一个数据迁移功能,可以根据名字猜测性别。从类中删除first_data成员显然会破坏此代码。但是,即使添加新的name_数据成员也会造成问题,因为当我们试图更新对象以存储新的性别时,ODB也会尝试更新name_。但是数据库中还没有相应的列。当我们运行此迁移函数时,距离添加名称列的位置还有几个版本。

  • 这是一个非常微妙但也非常重要的含义。与只需要处理当前模型版本的主应用程序逻辑不同,数据迁移代码适用于可能落后于当前版本的多个版本的数据库。

  • 我们如何解决这个问题?似乎我们需要的是从特定版本开始添加或删除数据成员的能力。在ODB中,这种机制称为软成员添加和删除。软添加成员仅从添加版本开始被视为持久成员。软删除成员在删除版本之前是持久的(但包括迁移阶段)。本质上,软模型更改允许我们使用一组持久类来维护对象模型的多个版本。现在让我们看看这个功能如何帮助实现我们的更改:

#pragma db model version(1, 4)

#pragma db object
class person
{
  ...

  #pragma db id auto
  unsigned long id_;

  #pragma db deleted(4)
  std::string first_;

  #pragma db deleted(4)
  std::string middle_;

  #pragma db deleted(4)
  std::string last_;

  #pragma db added(4)
  std::string name_;

  gender gender_;
};
  • 此更改的迁移功能可能如下:
static void
migrate_name (odb::database& db)
{
  for (person& p: db.query<person> ())
  {
    p.name (p.first () + " " +
            p.middle () + (p.middle ().empty () ? "" : " ") +
            p.last ());
    db.update (p);
  }
}

static const odb::data_migration_entry<4, MYAPP_BASE_VERSION>
migrate_name_entry (&migrate_name);
  • 另请注意,性别迁移功能不需要更改。

  • 您可能已经注意到,在上面的代码中,我们假设person类仍然为现在删除的数据成员提供公共访问器。这可能并不理想,因为现在它们不应该被应用程序逻辑使用。可能仍然需要访问它们的唯一代码是迁移函数。解决这个问题的建议方法是删除与已删除数据成员对应的访问器/修饰符,使迁移函数成为被迁移类的静态函数,然后直接访问已删除的数据成员。例如:

#pragma db model version(1, 4)

#pragma db object
class person
{
  ...

private:
  friend class odb::access;

  #pragma db id auto
  unsigned long id_;

  #pragma db deleted(4)
  std::string first_;

  #pragma db deleted(4)
  std::string middle_;

  #pragma db deleted(4)
  std::string last_;

  #pragma db added(4)
  std::string name_;

  gender gender_;

private:
  static void
  migrate_gender (odb::database&);

  static void
  migrate_name (odb::database&);
};

void person::
migrate_gender (odb::database& db)
{
  for (person& p: db.query<person> ())
  {
    p.gender_ = guess_gender (p.first_);
    db.update (p);
  }
}

static const odb::data_migration_entry<3, MYAPP_BASE_VERSION>
migrate_name_entry (&migrate_gender);

void person::
migrate_name (odb::database& db)
{
  for (person& p: db.query<person> ())
  {
    p.name_ = p.first_ + " " +
              p.middle_ + (p.middle_.empty () ? "" : " ") +
              p.last_;
    db.update (p);
  }
}

static const odb::data_migration_entry<4, MYAPP_BASE_VERSION>
migrate_name_entry (&migrate_name);
  • 软删除的另一个潜在问题是要求将删除的数据成员保留在类中。虽然它们不会在应用程序的正常操作中初始化(即不是迁移),但如果我们需要最小化类的内存占用,这仍然是一个问题。例如,我们可能会在内存中缓存大量对象,并且拥有三个std::string数据成员可能会带来很大的开销。

  • 解决此问题的建议方法是将所有已删除的数据成员放入动态分配的复合值类型中。例如:

#pragma db model version(1, 4)

#pragma db object
class person
{
  ...

  #pragma db id auto
  unsigned long id_;

  #pragma db added(4)
  std::string name_;

  gender gender_;

  #pragma db value
  struct deleted_data
  {
    #pragma db deleted(4)
    std::string first_;

    #pragma db deleted(4)
    std::string middle_;

    #pragma db deleted(4)
    std::string last_;
  };

  #pragma db column("")
  std::unique_ptr<deleted_data> dd_;

  ...
};
  • 如果加载了任何已删除的数据成员,ODB将自动分配已删除的值类型。然而,在正常操作期间,指针将保持NULL,因此将常见情况开销减少到每个类一个指针。请注意,我们将复合值列前缀设为空(db column("") pragma),以便为已删除的数据成员保留相同的列名。

  • 软添加和删除的数据成员可用于对象、复合值、视图和容器值类型。我们还可以软添加和删除简单、复合、指向对象的指针和容器类型的数据成员。只有特殊的数据成员,如对象id和乐观并发版本,不能软添加或删除。

  • 也可以软删除持久类。我们仍然可以使用这样一个类的现有对象,但是,在新数据库中不会为软删除的类创建表。换句话说,软删除类就像一个抽象类(没有表),但仍然可以加载、更新等。软添加的持久类没有多大意义,因此不受支持。

  • 作为软删除类的一个例子,假设我们想用新的employee对象替换person类并迁移数据。我们可以这样做:

#pragma db model version(1, 5)

#pragma db object deleted(5)
class person
{
  ...
};

#pragma db object
class employee
{
  ...

  #pragma db id auto
  unsigned long id_;

  std::string name_;
  gender gender_;

  static void
  migrate_person (odb::database&);
};

void employee::
migrate_person (odb::database& db)
{
  for (person& p: db.query<person> ())
  {
    employee e (p.name (), p.gender ());
    db.persist (e);
  }
}

static const odb::data_migration_entry<5, MYAPP_BASE_VERSION>
migrate_person_entry (&migrate_person);
  • 正如我们上面看到的,硬成员的添加和删除会(而且很可能会)破坏现有的数据迁移代码。那么,为什么不把所有的变化,或者至少是增加的内容,都当作软的呢?ODB要求您明确请求此语义,因为支持软添加和删除数据成员会产生运行时开销。在许多情况下,可能没有现有的数据迁移,因此硬添加和删除就足够了。

  • 在某些情况下,硬添加或删除会导致编译时错误。例如,其中一个数据迁移函数可能会引用我们刚刚删除的数据成员。然而,在许多情况下,这些错误只能在运行时检测到,更糟糕的是,只有在执行迁移功能时才能检测到。例如,我们可能很难添加一个新的数据成员,现有的迁移函数将尝试将其作为对象更新的一部分间接存储在数据库中。因此,强烈建议您始终使用从基本版本开始的数据库测试应用程序,以便调用每个数据迁移函数,从而确保其仍然正常工作。

  • 为了帮助解决这个问题,您还可以指示ODB使用--warn-hard-add, --warn-hard-delete, 和 --warn-hard硬命令行选项警告您任何硬添加或删除。ODB只会在当前版本中的硬更改时警告您,并且只在它打开的时候警告您,这使得此机制相当可用。

  • 您可能还想知道为什么我们必须明确指定添加和删除版本。看起来ODB编译器应该能够自动解决这个问题。虽然理论上是可能的,但为了实现这一点,ODB除了已经维护的数据库模式更改日志外,还必须维护C++对象模型的单独更改日志。虽然要复杂得多,但这样一个额外的变更日志也会使工作流程变得非常复杂。因此,将此更改信息作为原始源文件的一部分进行维护似乎是一种更干净、更简单的方法。

  • 正如我们之前讨论的,当我们向前移动基础模型版本时,我们基本上放弃了对新基础之前版本的迁移的支持。因此,在新的基础版本之前(包括在内),不再需要维护增删操作的软语义。ODB将为所有此类成员和类发布诊断。对于软删除,我们可以简单地完全删除数据成员或类。对于软添加,我们只需要删除db added pragma。

13.4.1 重用继承更改

  • 除了添加和删除数据成员外,更改对象表的另一种方法是使用重用样式继承。如果我们添加一个新的重用库,那么从数据库模式的角度来看,这相当于将其所有列添加到派生对象的表中。同样,删除重用继承会导致从派生表中删除基的所有列。

  • 未来,ODB可能会为继承的软添加和删除提供直接支持。然而,目前,这种语义可以通过软添加和删除数据成员来模拟。下表描述了最常见的场景,具体取决于添加或删除列的位置,即基表、派生表或两者兼而有之。
    在这里插入图片描述
    在这里插入图片描述

13.4.2 多态性继承更改

  • 与重用继承不同,添加或删除多态库不会导致从派生对象的表中添加或删除库的数据成员,因为多态层次结构中的每个类都存储在单独的表中。然而,由于存在特殊列(根表中的鉴别器和派生表中的对象id链接),这使得更改层次结构难以自动处理,因此还有其他复杂性。完全支持在多态层次结构中添加或删除(包括软删除)叶类(或叶子层次结构)。任何更复杂的更改,例如添加或删除根或中间基,或将现有类移入或移出多态层次结构,都可以通过创建新的叶子类(或叶子子层次结构)、软删除旧类和迁移数据来处理。
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值