从 Isar v3 升级到 Isar Plus v4
把旧的 isar 3.x 包升级到 isar_plus (v4) 属于破坏性文件格式更新。v4 核心写入的元数据与 v3 完全不同,因此无法打开旧数据库,会抛出如下错误:
VersionError: The database version is not compatible with this version of Isar.
解决办法是用旧运行时导出现有数据,再导入到一个全新的 Isar Plus 数据库中。下面的步骤会逐一说明。
迁移流程概览
- 先发布(或保留)一个仍依赖
isar:^3.1.0+1的版本,用它来读取旧文件。 - 在迁移期间,把
isar_plus与isar_plus_flutter_libs和旧包并行加入项目。 - 重新运行代码生成器,让所有 Schema 针对 v4 API 重新编译。
- 将 v3 实例里的每条记录复制到全新的 Isar Plus 实例中。
- 复制完成后删除旧文件,并移除旧依赖。
如果你不需要旧数据,可以直接删除 v3 目录并重新创建空数据库。本文其余部分专注于保留已有数据。
并行更新依赖
在复制完成之前保留旧运行时,之后再引入新的运行时:
dependencies:
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
isar_generator: ^3.1.0+1
isar_plus: ^1.1.5
isar_plus_flutter_libs: ^1.1.5
dev_dependencies:
build_runner: ^2.4.10
两个包导出的 Dart 符号相同,因此迁移时务必用别名导入:
import 'package:isar/isar.dart' as legacy;
import 'package:isar_plus/isar_plus.dart' as plus;
为 v4 重新生成 Schema
Isar Plus 自带生成器。重新运行 builder,就能吐出新的 helper 和适配器:
dart run build_runner build --delete-conflicting-outputs
此时暂停并处理所有编译错误(例如 Id? 字段需要改成非空 int id 或使用 Isar.autoIncrement)。API 迁移指南 总结了关键改动:
writeTxn()->writeAsync(),writeTxnSync()->write()txn()->readAsync(),txnSync()->read()- ID 必须叫
id或添加@id,自增使用Isar.autoIncrement @enumerated改名为@enumValue- 大部分旧链接由嵌入对象取代
复制真实数据
写一个一次性的迁移脚本(比如在 main() 初始化应用之前,或单独的 bin/migrate.dart)。通用思路:
- 使用 v3 运行时打开旧库。
- 在不同目录或不同名称下打开新的 v4 实例。
- 按页遍历每个集合,映射到新 Schema,并写入新库。
- 通过 SharedPreferences、本地文件或特性开关记录“已迁移”状态,避免重复执行。
Future<void> migrateLegacyDb(String directoryPath) async {
final legacyDb = await legacy.Isar.open(
[LegacyUserSchema, LegacyTodoSchema],
directory: directoryPath,
inspector: false,
name: 'legacy',
);
final plusDb = await plus.Isar.open(
[UserSchema, TodoSchema],
directory: directoryPath,
name: 'app_v4',
engine: plus.IsarEngine.sqlite, // 原生核心可使用 IsarEngine.isar
inspector: false,
);
await _copyUsers(legacyDb, plusDb);
await _copyTodos(legacyDb, plusDb);
await legacyDb.close();
await plusDb.close();
}
Future<void> _copyUsers(legacy.Isar legacyDb, plus.Isar plusDb) async {
const pageSize = 200;
final total = await legacyDb.legacyUsers.count();
for (var offset = 0; offset < total; offset += pageSize) {
final batch = await legacyDb.legacyUsers.where().offset(offset).limit(pageSize).findAll();
await plusDb.writeAsync((isar) async {
await isar.users.putAll(
batch.map((user) => User(
id: user.id ?? plus.Isar.autoIncrement,
email: user.email,
status: _mapStatus(user.status),
)),
);
});
}
}
提示:把
_mapStatus这类映射函数和迁移脚本放在一起,方便集中处理枚举改名、字段删除或数据清洗。
如果集合非常大,可以把循环放到 isolate 或后台服务中,以免阻塞 UI。嵌入对象和链接也能用同样方式搬运——用旧 API 读取,用新 Schema 持久化。
确保线上只跑一次
当应用同时包含两套运行时时,每次冷启动都有可能再次尝试迁移。记得把状态持久化,让复制过程在每次安装时只运行一次:
class MigrationTracker {
static const key = 'isarPlusMigration';
static Future<bool> needsMigration() async {
final prefs = await SharedPreferences.getInstance();
return !prefs.getBool(key).toString().contains('true');
}
static Future<void> markDone() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, true);
}
}
Future<void> bootstrapIsar(String dir) async {
if (await MigrationTracker.needsMigration()) {
await migrateLegacyDb(dir);
await MigrationTracker.markDone();
}
final isar = await plus.Isar.open(
[UserSchema, TodoSchema],
directory: dir,
);
runApp(MyApp(isar: isar));
}
如果预计未来还会迁移,可以存储一个数值版本(例如 3 表示旧库,4 表示 Isar Plus)。桌面或服务器环境也可以在数据库目录旁放一个 .migrated 文件作为标记。
后续清理
所有集合复制完毕后:
- 写入一个标记(如
prefs.setBool('migratedToIsarPlus', true))防止重复执行。 - 删除旧文件(手动或调用
plus.Isar.deleteDatabase(name: 'legacy', directory: directoryPath, engine: plus.IsarEngine.isar))。 - 从
pubspec.yaml移除isar与isar_flutter_libs依赖。 - 如有需要,把新数据库的名称/目录改回原来的名字。
只有在确信用户不会再打开旧版本后,才发布只依赖 isar_plus 的版本。
常见问题
- 仍然出现
VersionError:在打开 v4 实例前请删除所有 v3 文件,旧的 WAL/LCK 文件可能残留旧头部。 - 主键冲突:v4 要求 ID 是唯一且非空的整数,使用
Isar.autoIncrement或在复制时生成自定义主键。 - 代码生成失败:先运行
dart pub clean,再跑build_runner,并确认part '...g.dart';没有缺失。 - 需要回滚:迁移写入的是独立数据库,因此可以直接丢弃新文件,保留旧数据重新尝试。
完成这些步骤后,用户就能直接从 isar 3.x 升级到 isar_plus,不会丢失数据。