Flutter——数据库Drift开发详细教程之迁移(九)

迁移

入门

Drift 通过严格的架构确保查询类型安全。要更改此架构,您必须编写迁移。Drift 提供了一系列

API、命令行工具和测试实用程序,使编写和验证数据库迁移更加轻松可靠。

引导式迁移

Drift 提供内置工具来帮助您编写和测试迁移。这使您可以逐步编写架构更改,而测试实用程序则可以确保您的架构迁移正确无误。

配置

要使用该make-migrations 命令,您必须将数据库的位置添加到文件中build.yaml

  • 构建文件
dart 复制代码
targets:
  $default:
    builders:
      drift_dev:
        options:
          databases:
            # Required: A name for the database and its path
            my_database: lib/database.dart

            # Optional: Add more databases
            another_db: lib/database2.dart

您还可以选择指定测试文件和架构的目录文件已存储。

  • 构建文件
dart 复制代码
targets:
  $default:
    builders:
      drift_dev:
        options:
          # The directory where the test files are stored:
          test_dir: test/drift/ # (default)

          # The directory where the schema files are stored:
          schema_dir: drift_schemas/  # (default)

用法

在开始更改初始数据库模式之前,请运行此命令来生成初始模式文件。

dart 复制代码
dart run drift_dev make-migrations

保存此初始模式文件后,您可以开始更改数据库模式。

一旦您对更改感到满意,请schemaVersion在数据库类中添加并再次运行该命令。

dart 复制代码
dart run drift_dev make-migrations

该命令将生成以下文件:

您的数据库类旁边将生成一个分步迁移文件。使用此功能可以逐步编写迁移。有关更多信息,请参阅分步迁移指南。

Drift 还会为您的迁移生成一个测试文件。编写迁移代码后,请运行测试以验证迁移代码是否正确。该文件还包含首次迁移的示例数据完整性测试。

如果您在此过程中遇到困难,请不要犹豫,立即展开讨论。

例子

更改数据库架构并运行make-migrations 命令后,就该编写迁移了。如果您的数据库是在database.dart 文件中定义的,Drift 会在它旁边生成一个 database.steps.dart文件。该文件将包含所有后续架构版本的压缩表示,这使得编写迁移更加容易:

  • 数据库.dart
dart 复制代码
import 'package:drift/drift.dart';

import 'database.steps.dart';

part 'database.g.dart';

@DriftDatabase(...)
class MyDatabase extends _$MyDatabase {
  MyDatabase(super.e);

  @override
  int get schemaVersion => 2; // bump because the tables have changed.

  @override
  MigrationStrategy get migration {
    return MigrationStrategy(
      onUpgrade: stepByStep(
        from1To2: (m, schema) async {
          await m.createTable(schema.groups);
        },
      ),
    );
  }
}

请参阅漂移存储库中的示例,以获取有关如何使用该make-migrations命令的完整示例。

切换到make-migrations

如果您已经使用该schema工具来编写迁移,则可以make-migrations按照以下步骤进行切换:

运行make-migrations命令生成初始模式文件。

将所有现有schema文件移动到数据库的架构目录中。

再次运行make-migrations命令,生成分步迁移文件和测试文件。

开发过程中

在开发过程中,您可能会频繁更改架构,并且暂时不想为此编写迁移。您可以删除应用数据并重新安装应用 -

数据库将被删除,所有表将重新创建。请注意,有时卸载是不够的 - Android 系统可能已备份数据库文件,并在重新安装应用时重新创建。

您还可以在每次打开应用程序时删除并重新创建所有表,请参阅此评论 以了解如何实现此目的。

手动迁移

手动迁移容易出错

手动编写迁移文件容易出错,甚至可能导致数据丢失。我们建议使用make-migrations命令生成迁移文件和测试。

Drift 提供了一个迁移 API ,可用于在修改类schemaVersion 中的 getter 后逐步应用架构更改Database 。要使用该 API,请重写migration getter

举个例子:假设你想在待办事项条目(v2Schema 中的)中添加截止日期。之后,你又决定添加优先级列(v3Schema 中的)。

dart 复制代码
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 6, max: 10)();
  TextColumn get content => text().named('body')();
  IntColumn get category => integer().nullable()();
  DateTimeColumn get dueDate =>
      dateTime().nullable()(); // new, added column in v2
  IntColumn get priority => integer().nullable()(); // new, added column in v3
}

我们现在可以database像这样改变类:

dart 复制代码
@override
int get schemaVersion => 3; // bump because the tables have changed.

@override
MigrationStrategy get migration {
  return MigrationStrategy(
    onCreate: (Migrator m) async {
      await m.createAll();
    },
    onUpgrade: (Migrator m, int from, int to) async {
      if (from < 2) {
        // we added the dueDate property in the change from version 1 to
        // version 2
        await m.addColumn(todos, todos.dueDate);
      }
      if (from < 3) {
        // we added the priority property in the change from version 1 or 2
        // to version 3
        await m.addColumn(todos, todos.priority);
      }
    },
  );
}

// The rest of the class can stay the same

您还可以添加单个表或删除它们 - 请参阅Migrator的参考 以了解所有可用选项。

您还可以在迁移回调中使用更高级别的查询 API,例如selectupdate 或。但是,请注意,漂移在创建 SQL 语句或映射结果时需要最新的架构。例如,在向数据库添加新列时,您不应在实际添加该列之前在该表上运行 。通常,请尽量避免在迁移回调中运行查询。deleteselect

迁移后回调

参数beforeOpeninMigrationStrategy可用于在数据库创建后填充数据。它在迁移之后运行,但在任何其他查询之前运行。请注意,无论迁移是否实际运行,只要打开数据库,它都会被调用。您可以使用details.hadUpgradeordetails.wasCreated来检查迁移是否必要:

dart 复制代码
beforeOpen: (details) async {
    if (details.wasCreated) {
      final workId = await into(categories).insert(Category(description: 'Work'));

      await into(todos).insert(TodoEntry(
            content: 'A first todo entry',
            category: null,
            targetDate: DateTime.now(),
      ));

      await into(todos).insert(
            TodoEntry(
              content: 'Rework persistence code',
              category: workId,
              targetDate: DateTime.now().add(const Duration(days: 4)),
      ));
    }
},

您还可以激活您需要的编译指示语句:

dart 复制代码
beforeOpen: (details) async {
  if (details.wasCreated) {
    // ...
  }
  await customStatement('PRAGMA foreign_keys = ON');
}

导出模式

重要提示

此命令专用于导出架构。如果您正在使用此make-migrations命令,则此操作已为您完成。

按照设计,Drift 的代码生成器只能查看数据库架构的当前状态。更改架构时,将旧架构的快照存储在文件中会很有帮助。之后,Drift 工具可以查看所有架构文件,以验证您编写的迁移。

我们建议导出初始架构一次。之后,每个更改的架构版本(即每次更改schemaVersion数据库中的 )也应保存。本指南假设您的项目中有一个顶级drift_schemas/文件夹用于存储这些架构文件,如下所示:

dart 复制代码
my_app
  .../
  lib/
    database/
      database.dart
      database.g.dart
  test/
    generated_migrations/
      schema.dart
      schema_v1.dart
      schema_v2.dart
  drift_schemas/
    drift_schema_v1.json
    drift_schema_v2.json
  pubspec.yaml

当然,如果这更适合您的工作流程,您也可以使用其他文件夹或子文件夹。

导出模式并为其生成代码不能build_runner单独完成,这就是为什么这里描述的设置是必要的。

但我们希望这一切都值得!验证迁移可以确保你在更改数据库后不会遇到任何问题。如果你在迁移过程中遇到困难,请随时与我们讨论。

导出架构

首先,让我们创建第一个模式表示:

dart 复制代码
$ mkdir drift_schemas
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/

这指示生成器查看定义的数据库lib/database/database.dart并将其模式提取到新文件夹中。

更改数据库架构后,可以再次运行该命令。例如,假设我们对表进行了更改,并将 增加到schemaVersion。2要转储新架构,只需再次运行该命令:

dart 复制代码
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/

每次更改数据库架构并增加时,都需要运行此命令schemaVersion

Drift 会将文件夹中的文件命名为drift_schema_vX.json,其中X是数据库的当前路径schemaVersion 。如果 Drift 无法从schemaVersiongetter 中提取版本,请明确提供完整路径:

dart 复制代码
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json

转储数据库

如果您不想导出数据库类的模式,而是想导出现有 sqlite3 数据库文件的模式,那么您也可以这样做!drift_dev schema dump将 sqlite3 数据库文件识别为其第一个参数,并可以从那里提取相关模式。

下一步是什么?

将您的架构版本导出到这样的文件中后,漂移工具能够生成可识别多个架构版本的代码。

这使得逐步迁移成为可能:Drift 可以为您需要编写的每个架构迁移生成样板代码,这样您只需填写实际更改的内容。这使得编写迁移变得更加容易。

通过了解所有模式版本,drift 还可以生成测试代码,这使得为所有模式迁移编写单元测试变得容易。

调试导出架构的问题

警告

这部分详细描述了导出模式时的一个具体问题,但与大多数用户无关。当 Drift 遇到此处描述的问题时,会引导您参考本节。

首先,drift 是一个代码生成器:它根据你的定义生成 Dart 类,这些定义负责CREATE TABLE在运行时构造语句。虽然 Drift 只需查看你的代码就能很好地了解你的数据库模式(这是生成类型安全代码所必需的能力),但有些细节需要运行 Dart 才能实现。

一个常见的例子是默认值:它们通常像这样定义,可能很容易分析:

dart 复制代码
class Entries extends Table {
  TextColumn get content => text().withDefault(const Constant('test'))();
}
但是,没有什么可以阻止你这样做:


String computeDefaultContent() {
  // ...
}

class Entries extends Table {
  TextColumn get content => text().withDefault(Constant(computeDefaultContent()))();
}

由于漂移模式旨在完整表示您的数据库(其中包括默认值),因此我们必须运行部分代码,例如computeDefaultContent!

在较旧的漂移版本中,这个问题是通过将源代码(例如computeDefaultContent ())嵌入到模式文件中来解决的。生成迁移代码时,该代码还会调用,computeDefaultContent这将在运行时恢复模式(允许漂移将其与实际模式进行比较以进行测试)。这种方法有两个致命的缺陷:

当您稍后删除该computeDefaultContent 方法时,因为更高的架构版本不再需要它,旧架构版本生成的代码将调用不存在的方法。

当您稍后更改 的实现时computeDefaultContent,这会隐式地更改架构(并需要迁移)。但是,由于漂移架构只知道computeDefaultContent()需要评估 ,因此它们不知道在旧架构中评估的computeDefaultContent() 结果是什么。

为了解决这个问题,drift 会尝试运行部分数据库代码。因此,如果当时computeDefaultContent() 计算结果为或在命令行中调用,drift 会在内部重写表,如下所示:hello worldmake-migrationsschema export

dart 复制代码
class Entries extends Table {
  TextColumn get content => text().withDefault(const Constant('hello world'))();
}

当您稍后更改或删除时computeDefaultContent(),这不会影响模式测试的正确性。

因此,虽然从 CLI 运行部分数据库代码是良好模式导出的必要条件,但不幸的是,这并非没有问题。您drift_dev使用运行dart,但您的应用很可能会导入 Flutter 特定的 API:

dart 复制代码
import 'package:flutter/material.dart' show Colors;

class Users extends Table {
  IntColumn get profileBackgroundColor =>
      integer().withDefault(Constant(Colors.red.shade600.value))();
}

要在此处评估默认值,我们必须评估Colors.red.shade600.value。由于 Color定义在 中dart:ui(Dart CLI 应用程序不可用),drift 将无法分析架构。由于 Dart 中导入的工作方式,在导入 Flutter 的 Dart 文件中定义常量时,这也可能是一个问题:

dart 复制代码
// constants.dart
import 'package:flutter/flutter.dart';

const defaultUserName = 'name';

import 'constants.dart';

class Users extends Table {
  // This is a problem: make-migrations has to import constants.dart, which depends on Flutter
  TextColumn get name => text().withDefault(const Constant(defaultUserName))();
}

另一方面,单独将 Flutter 导入数据库文件是没有问题的:

dart 复制代码
import 'package:drift_flutter/drift_flutter.dart';

class Users extends Table {
  // Not a problem; withDefault only references core drift APIs (`Constant`)
  TextColumn get name => text().withDefault(const Constant('name'))();
}

Drift 会移除那些不影响 schema 的内容(例如clientDefault 、 或类型转换器)。这些都可以自由地导入 Flutter 特定的 API。

修复这个问题

当漂移无法分析您的模式并打印指向文档此部分的异常时,通常可以重构您的代码以避免出现问题。

首先,有必要了解问题所在。您可以指示 Drift 转储用于运行数据库的内部代码:

dart 复制代码
dart run drift_dev make-migrations --export-schema-startup-code=schema_description.dart

接下来,尝试自己运行此代码来重现错误:

dart 复制代码
dart run schema_description.dart

Dart 编译器将在指向不受支持的库(如 Flutter)的路径上打印解释。

这样,就有可能消除它们:

检查为什么 Drift 会导入 Flutter 专用文件。它是否定义了默认值中使用的方法或字段?

尝试重构您的代码,以便将这些定义移动到不导入 Flutter 的文件中。

重复!

虽然我们希望这不会影响大多数用户,但这个问题的解决可能比较困难。如果您需要更多指导,请在此问题下发表评论。

架构迁移助手

数据库迁移通常是增量式编写的,只需一段代码即可将数据库架构迁移到下一个版本。通过串联这些迁移,即使对于非常旧的应用版本,您也可以编写架构迁移。

然而,在应用版本之间可靠地编写迁移代码并不容易。这些代码需要维护和测试,但数据库架构日益复杂,迁移不应该变得更加复杂。让我们来看一个典型的例子,它使得增量迁移模式变得困难:

  1. 在初始数据库模式中,我们有一堆表。
  2. 在从 1 到 2 的迁移中,我们birthDate向其中一个表添加了一列(Users)。
  3. 在版本 3 中,我们意识到我们实际上根本不想存储用户并删除该表。

在版本 3 之前,唯一的迁移方式是m.addColumn(users, users.birthDate) 。但现在Users源代码中已经没有这个表了,所以这已经不可能了!当然,我们可以记住从 1 到 2 的迁移现在毫无意义,如果用户直接从 1 升级到 3,就可以跳过它,但这会增加很多复杂性。对于跨多个版本的更复杂的迁移脚本,这很快就会导致代码难以理解和维护。

如果您正在使用make-migrations命令或其他导出架构的漂移工具,则可以指示漂移为所有架构版本生成最少的代码。这是 make-migrations命令的一部分,但此步骤也可以手动调用。

生成的文件(在数据库文件旁边生成)定义了一个stepByStep可以传递给的实用程序onUpgrade:

数据库.dart

dart 复制代码
// This is the default file generated by make-migrations. When you invoke `drift_dev schema steps`
// manually, you control the name and path of the generated file.
import 'database.steps.dart';

@DriftDatabase()
class Database extends _$Database {
  // Constructor and other methods

  @override
  MigrationStrategy get migration {
    return MigrationStrategy(
      onUpgrade: stepByStep(
        from1To2: (m, schema) async {
          await m.addColumn(schema.users, schema.users.birthdate);
        },
        from2To3: (m, schema) async {
          await m.deleteTable('users');
        },
      ),
    );
  }
}

这大大简化了迁移的编写,因为:

  1. 每个fromXToY函数都可以访问它要迁移到的架构。因此,schema.users尽管用户表随后被删除,但在从版本一到版本二的迁移过程中,它们仍然可用。
  2. 迁移被构建为独立的功能,使其更易于维护。
  3. 您可以继续测试向旧模式版本的迁移,让您确信迁移是正确的,并且从不同版本升级的用户不会遇到问题。

自定义分步迁移

stepByStep 该命令生成的函数会drift_dev schema steps提供 OnUpgrade回调。但您可能希望自定义升级行为,例如在升级后添加外键检查。

Migrator.runMigrationSteps可以使用辅助方法来实现这一点,如下例所示:

dart 复制代码
onUpgrade: (m, from, to) async {
  // Run migration steps without foreign keys and re-enable them later
  // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
  await customStatement('PRAGMA foreign_keys = OFF');

  await m.runMigrationSteps(
    from: from,
    to: to,
    steps: migrationSteps(
      from1To2: (m, schema) async {
        // we added the dueDate property in the change from version 1 to
        // version 2
        await m.addColumn(schema.todos, schema.todos.dueDate);
      },
      from2To3: (m, schema) async {
        // we added the priority property in the change from version 1 or 2
        // to version 3
        await m.addColumn(schema.todos, schema.todos.priority);
      },
    ),
  );

  if (kDebugMode) {
    // Fail if the migration broke foreign keys
    final wrongForeignKeys =
        await customSelect('PRAGMA foreign_key_check').get();
    assert(wrongForeignKeys.isEmpty,
        '${wrongForeignKeys.map((e) => e.data)}');
  }

  await customStatement('PRAGMA foreign_keys = ON;');
},

在这里,外键在运行迁移之前被禁用,并在运行迁移之后重新启用。检查确保没有发生不一致有助于在调试模式下捕获迁移问题。

转向逐步迁移

如果您之前曾使用过添加到库中的漂移,或者您从未导出过模式,则可以通过将值固定到已知的起点来stepByStep 进行逐步迁移。fromMigrator.runMigrationSteps

这使您可以执行所有先前的迁移工作,以使数据库到达迁移的"起点" stepByStep ,然后使用stepByStep该架构版本之外的迁移。

onUpgrade: (m, from, to) async {

// Run migration steps without foreign keys and re-enable them later

// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)

await customStatement('PRAGMA foreign_keys = OFF');

// Manually running migrations up to schema version 2, after which we've

// enabled step-by-step migrations.

if (from < 2) {

// we added the dueDate property in the change from version 1 to

// version 2 - before switching to step-by-step migrations.

await m.addColumn(todos, todos.dueDate);

}

// At this point, we should be migrated to schema 3. For future schema

// changes, we will "start" at schema 3.

await m.runMigrationSteps(

from: math.max(2, from),

to: to,

steps: migrationSteps(

from2To3: (m, schema) async {

// we added the priority property in the change from version 1 or

// 2 to version 3

await m.addColumn(schema.todos, schema.todos.priority);

},

),

);

复制代码
    if (kDebugMode) {
      // Fail if the migration broke foreign keys
      final wrongForeignKeys =
          await customSelect('PRAGMA foreign_key_check').get();
      assert(wrongForeignKeys.isEmpty,
          '${wrongForeignKeys.map((e) => e.data)}');
    }

    await customStatement('PRAGMA foreign_keys = ON;');
  },

from这里,我们给 的值设定了一个"下限" 2,因为我们已经完成了所有其他迁移工作,才到达了这一点。从现在开始,您可以为每个架构变更生成分步迁移。

如果您不这样做,从架构 1 直接迁移到架构 3 的用户将无法正确完成迁移并应用所需的所有迁移更改。

手动生成

重要提示

此命令专门用于生成分步迁移助手。如果您正在使用此make-migrations命令,则此操作已为您完成。

Drift 提供了导出旧模式版本的工具。导出所有模式版本后,您可以使用以下命令生成代码,以帮助实现分步迁移:

dart 复制代码
$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart

第一个参数 ( drift_schemas/) 是存储导出模式的文件夹,第二个参数是要生成的文件的路径。通常,你会在数据库类旁边生成一个文件。

生成的文件包含一个stepByStep可用于轻松编写迁移的方法:

dart 复制代码
// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`
import 'schema_versions.dart';

MigrationStrategy get migration {
  return MigrationStrategy(
    onCreate: (Migrator m) async {
      await m.createAll();
    },
    onUpgrade: stepByStep(
      from1To2: (m, schema) async {
        // we added the dueDate property in the change from version 1 to
        // version 2
        await m.addColumn(schema.todos, schema.todos.dueDate);
      },
      from2To3: (m, schema) async {
        // we added the priority property in the change from version 1 or 2
        // to version 3
        await m.addColumn(schema.todos, schema.todos.priority);
      },
    ),
  );
}

stepByStep 每次负责运行部分迁移的模式升级都需要一个回调。该回调接收两个参数:一个迁移器m(类似于回调中获取的常规迁移器 onUpgrade )和一个schema 允许您访问要迁移到的版本模式的参数。例如,在from1To2 函数中,schema提供了版本 2 的数据库模式的 getter。传递给该函数的迁移器也设置为默认考虑该特定版本。例如,调用**m.recreateAllViews()**会以预期的模式版本 2 状态重新创建视图。

测试迁移

重要提示

如果您正在使用该make-migrations命令,则测试已经为您生成。

虽然可以手动编写迁移而无需额外的漂移帮助,但测试迁移的专用工具有助于确保它们是正确的并且不会丢失任何数据。

Drift 的迁移工具包括以下步骤:

每次更改模式后,请使用工具将当前模式导出到单独的文件中。

使用漂移工具生成测试代码,以验证您的迁移是否将数据库带入预期的模式。

使用生成的代码使编写模式迁移变得更容易。

本页介绍了步骤 2 和 3。假设您已按照步骤 1 操作, 即在架构发生更改时导出架构。另请参阅关于使用漂移编写单元测试的通用页面。具体来说,您可能需要sqlite3在系统上手动安装,因为sqlite3_flutter_libs这不适用于单元测试。

编写测试

将数据库架构导出到文件夹后,您可以基于这些架构文件生成数据库类的旧版本。为了进行验证,drift 将生成一个更小的数据库实现,该实现只能用于测试迁移。

你可以把这段测试代码放在任何你想要的地方,但最好把它放在 的子文件夹中test/。如果我们想把它们写入test/generated_migrations/,我们可以使用

dart 复制代码
$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/

设置完成后,终于可以编写一些测试了!例如,测试可以如下所示:

dart 复制代码
import 'package:test/test.dart';
import 'package:drift_dev/api/migrations_native.dart';

// The generated directory from before.
import 'generated_migrations/schema.dart';


void main() {
  late SchemaVerifier verifier;

  setUpAll(() {
    // GeneratedHelper() was generated by drift, the verifier is an api
    // provided by drift_dev.
    verifier = SchemaVerifier(GeneratedHelper());
  });

  test('upgrade from v1 to v2', () async {
    // Use startAt to obtain a database connection with all tables
    // from the v1 schema.
    final connection = await verifier.startAt(1);
    final db = MyDatabase(connection);

    // Use this to run a migration to v2 and then validate that the
    // database has the expected schema.
    await verifier.migrateAndValidate(db, 2);
  });
}

一般来说,测试如下所示:

migrateAndValidate 将从表中提取所有CREATE 语句sqlite_schema 并进行语义比较。如果发现任何异常,它将抛出SchemaMismatch异常,导致测试失败。

编写可测试的迁移

要测试向旧架构版本的迁移(例如,如果当前版本为 ,则从v1迁移到),您的处理程序必须能够升级到比当前版本更旧的版本。为此,请检查回调函数的参数,以便在必要时运行不同的迁移。或者,使用分步迁移功能,它可以自动执行此操作。v2v3onUpgradeschemaVersiontoonUpgrade

验证数据完整性

除了对表结构所做的更改之外,确保迁移之前存在的数据在迁移运行后仍然存在也很有用。除了连接之外,您还可以使用从包中schemaAt 获取原始数据。这可用于在迁移之前插入数据。迁移运行后,您可以检查数据是否仍然存在。Databasesqlite3

请注意,您无法使用应用中的常规数据库类来实现此目的,因为它的数据类始终需要最新的架构。但是,您可以指示 Drift 生成数据类及其伴随对象的旧快照,以实现此目的。要启用此功能,请将--data-classes和--companions命令行参数传递给以下drift_dev schema generate 命令:

dart 复制代码
$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/

然后,您可以使用别名导入生成的类:

dart 复制代码
import 'generated_migrations/schema_v1.dart' as v1;
import 'generated_migrations/schema_v2.dart' as v2;

然后可以使用它来手动创建和验证特定版本的数据:

dart 复制代码
void main() {
  // ...
  test('upgrade from v1 to v2', () async {
    final schema = await verifier.schemaAt(1);

    // Add some data to the table being migrated
    final oldDb = v1.DatabaseAtV1(schema.newConnection());
    await oldDb.into(oldDb.todos).insert(v1.TodosCompanion.insert(
          title: 'my first todo entry',
          content: 'should still be there after the migration',
        ));
    await oldDb.close();

    // Run the migration and verify that it adds the name column.
    final db = MyDatabase(schema.newConnection());
    await verifier.migrateAndValidate(db, 2);
    await db.close();

    // Make sure the entry is still here
    final migratedDb = v2.DatabaseAtV2(schema.newConnection());
    final entry = await migratedDb.select(migratedDb.todos).getSingle();
    expect(entry.id, 1);
    expect(entry.dueDate, isNull); // default from the migration
    await migratedDb.close();
  });
}

在运行时验证数据库模式

除了编写测试以确保迁移按预期工作 之外,drift_dev还提供 API 来在运行时验证当前模式,而无需在本机平台上进行任何额外的设置。

本国的

Web(自 Drift 2.22 起)

dart 复制代码
// import the migrations tooling
import 'package:drift_dev/api/migrations_native.dart';

class MyDatabase extends _$MyDatabase {
  @override
  MigrationStrategy get migration => MigrationStrategy(
        onCreate: (m) async {/* ... */},
        onUpgrade: (m, from, to) async {/* your existing migration logic */},
        beforeOpen: (details) async {
          // your existing beforeOpen callback, enable foreign keys, etc.

          if (kDebugMode) {
            // This check pulls in a fair amount of code that's not needed
            // anywhere else, so we recommend only doing it in debug builds.
            await validateDatabaseSchema();
          }
        },
      );
}

当您使用时validateDatabaseSchema,漂移将透明地:

通过读取来收集有关数据库的信息sqlite3_schema

创建数据库的全新内存实例并使用创建参考模式Migrator.createAll()

比较两者。理想情况下,即使实际的架构在应用程序的不同版本中不断演变,运行时的实际架构也应该与最新的架构相同。

当发现不匹配时,会抛出异常,并显示一条消息,准确解释应为另一个值的位置。这能让您快速找到架构迁移中的问题。

也可在 DevTools 中使用

确保当前架构与预期状态匹配也是 Drift 的 DevTools 扩展中的一项功能。该扩展还允许重置数据库,这在处理或调试迁移时可能很有用。

迁移器 API

您可以使用迁移回调手动编写迁移customStatement()。但是,回调还会提供一个实例Migrator作为参数。该类了解数据库的目标架构,并可用于创建、删除和修改架构中的大多数元素。

一般提示

为了确保架构在迁移期间保持一致,您可以将其包装在transaction块中。但是,请注意,某些指令(包括foreign_keys)无法在事务内部更改。不过,以下做法仍然很有用:

在使用数据库之前,请务必重新启用外键,方法是在中启用它们beforeOpen。

迁移之前禁用外键。

在事务内运行迁移。

确保您的迁移没有引入任何不一致PRAGMA foreign_key_check。

将所有这些结合起来,迁移回调看起来就像这样:

dart 复制代码
return MigrationStrategy(
  onUpgrade: (m, from, to) async {
    // disable foreign_keys before migrations
    await customStatement('PRAGMA foreign_keys = OFF');

    await transaction(() async {
      // put your migration logic here
    });

    // Assert that the schema is valid after migrations
    if (kDebugMode) {
      final wrongForeignKeys =
          await customSelect('PRAGMA foreign_key_check').get();
      assert(wrongForeignKeys.isEmpty,
          '${wrongForeignKeys.map((e) => e.data)}');
    }
  },
  beforeOpen: (details) async {
    await customStatement('PRAGMA foreign_keys = ON');
    // ....
  },
);

迁移视图、触发器和索引

更改视图、触发器或索引的定义时,更新数据库架构最简单的方法是删除并重新创建元素。使用MigratorAPI ,只需调用,await drop(element) 然后按await create(element),其中element是要更新的触发器、视图或索引。

请注意,Dart 定义的视图定义可能会发生变化,而无需修改视图类本身。这是因为表中的列是通过 getter 引用的。如果.named('name')在表定义中重命名列而不重命名 getter,Dart 中的视图定义会保持不变,但 CREATE VIEW语句会发生变化。

解决这个问题的一个简单方法是重新创建迁移中的所有视图,为此Migrator 提供了recreateAllViews方法。

复杂的迁移

SQLite 内置了用于简单更改的语句,例如添加列或删除整个表。更复杂的迁移需要12 个步骤,包括创建表的副本并从旧表中复制数据。Drift 2.4 引入了TableMigrationAPI 来自动化大部分此类过程,使其更易于使用且更安全。

要开始迁移,drift 将使用当前架构创建表的新实例。接下来,它将从旧表中复制行。在大多数情况下,例如在更改列类型时,我们不能只复制每一行而不更改其内容。此时,您可以使用 来columnTransformer 应用每行转换。columnTransformer是从列到 SQL 表达式的映射,该表达式将用于从旧表中复制列。例如,如果我们想在复制列之前对其进行类型转换,我们可以使用:

dart 复制代码
columnTransformer: {
  todos.category: todos.category.cast<int>(),
}

在内部,Drift 将使用INSERT INTO SELECT语句复制旧数据。在本例中,它看起来像 INSERT INTO temporary_todos_copy SELECT id, title, content, CAST(category AS INT) FROM todos。如您所见,Drift 将使用columnTransformer 映射中的表达式,否则将回退到仅复制列。如果您在表迁移中引入新列,请确保将它们包含在newColumns 的参数 中TableMigration。Drift 将确保这些列具有默认值或 的转换columnTransformer 。当然,DriftnewColumns也不会尝试从旧表复制。

无论您是使用复杂的迁移TableMigration还是通过运行自定义语句序列来实现迁移,我们都强烈建议您编写涵盖迁移的集成测试。这有助于避免迁移错误导致的数据丢失。

以下是一些展示表迁移 API 常见用法的示例:

更改列的类型

假设category中的列以前Todos是不可为空的text()列,现在我们要将其更改为可为空的 int 类型。为简单起见,我们假设 中category始终包含整数,它们只是存储在我们现在想要调整的文本列中。

dart 复制代码
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 6, max: 10)();
  TextColumn get content => text().named('body')();
-  TextColumn get category => text()();
+  IntColumn get category => integer().nullable()();
}

重新运行构建并增加架构版本后,您可以编写迁移:

dart 复制代码
return MigrationStrategy(
  onUpgrade: (m, old, to) async {
    if (old <= yourOldVersion) {
      await m.alterTable(
        TableMigration(todos, columnTransformer: {
          todos.category: todos.category.cast<int>(),
        }),
      );
    }
  },
);

这里最重要的部分是columnTransformer ------一个从列到表达式的映射,用于复制旧数据。该映射中的值引用旧表,因此我们可以用它来 todos.category.cast()复制旧行并对其进行转换category 。所有不存在的列都columnTransformer将从旧表中复制,而无需进行任何转换。

更改列约束

当您以与现有数据兼容的方式更改列约束(例如,将非可空列更改为可空列)时,您可以直接复制数据而不应用任何转换:

dart 复制代码
await m.alterTable(TableMigration(todos));

删除列

删除未被外键约束引用的列也很容易:

dart 复制代码
await m.alterTable(TableMigration(yourTable));

要删除外键引用的列,您必须先迁移引用表。

重命名列

如果您要在 Dart 中重命名列,请注意最简单的方法是重命名 getter 并使用 named: TextColumn newName => text().named('old_name')()。这完全向后兼容,不需要迁移。

如果您知道您的应用程序在 sqlite 3.25.0 或更高版本上运行(如果您使用的话sqlite3_flutter_libs),您也可以使用renameColumnapi Migrator:

dart 复制代码
m.renameColumn(yourTable, 'old_column_name', yourTable.newColumn);

如果您确实想更改表中的实际列名,则可以编写一个columnTransformer使用不同名称的旧列:

dart 复制代码
await m.alterTable(
  TableMigration(
    yourTable,
    columnTransformer: {
      yourTable.newColumn: const CustomExpression('old_column_name')
    },
  )
)

合并列

要添加一个应具有根据多个现有列计算出的默认值的列,您也可以使用 API 来表达TableMigration

dart 复制代码
await m.alterTable(
  TableMigration(
    yourTable,
    columnTransformer: {
      yourTable.newColumn: Variable.withString('from previous row: ') +
          yourTable.oldColumn1 +
          yourTable.oldColumn2.upper()
    },
  )
)

添加新列

添加新列的最简单方法是使用addColumn迁移器上的方法:

dart 复制代码
await m.addColumn(users, users.middleName);

但在某些情况下,这还不够。特别是,如果你要添加以下列:

不可为空,并且

没有默认值(clientDefault 此处不算作默认值),并且

不是自动递增主键。

那么此列就无法安全地添加到现有表中,因为数据库不知道在现有行中应该使用哪个值。通过使用alterTableAPI ,您可以指定一个仅适用于现有行的默认值(新插入的数据要么获取clientDefault默认值,要么需要为新列指定自己的值):

dart 复制代码
await m.alterTable(
  TableMigration(
    yourTable,
    columnTransformer: {
      yourTable.yourNewColumn: Constant('value for existing rows'),
    },
    newColumns: [yourTable.yourNewColumn],
  )
)
相关推荐
嘻哈baby17 小时前
MySQL主从复制与读写分离实战指南
数据库·mysql·adb
一水鉴天17 小时前
整体设计 定稿 之 5 讨论问题汇总 和新建 表述总表/项目结构表 文档分析,到读表工具核心设计讨论(豆包助手)
数据库·人工智能·重构
L、21817 小时前
Flutter 与开源鸿蒙(OpenHarmony)的融合开发实践
flutter·开源·harmonyos
我科绝伦(Huanhuan Zhou)17 小时前
Linux 环境下 SQL Server 自动收缩日志作业创建脚本(Shell 版)
linux·运维·数据库·sql server
TDengine (老段)17 小时前
山东港口科技借助 TDengine 构建智慧港口“数据基石”
大数据·数据库·物联网·时序数据库·tdengine
Hello.Reader17 小时前
Flink SQL INSERT 语句单表写入、多表分流、分区覆盖与 StatementSet
数据库·sql·flink
马克学长17 小时前
SSM小型餐饮综合管理系统j1c7m(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·小型餐饮管理系统·菜品管理·员工考勤
名字被你们想完了17 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(四)
flutter
爱吃大芒果17 小时前
Flutter 本地存储方案:SharedPreferences、SQFlite 与 Hive
开发语言·javascript·hive·hadoop·flutter·华为·harmonyos