开源鸿蒙 Flutter for OpenHarmony:sqflite避坑(锁库+迁移)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Day1/Day2 把离线笔记做到了“能用”,但一旦开始上强度(自动保存、快速删除、频繁切页),sqflite 最容易暴露两个问题:

  • 锁库/并发写:典型表现是 DatabaseException(database is locked)、偶发保存失败、列表刷新不及时
  • 升级迁移:表结构一改(加字段/加索引),旧数据如何平滑升级、升级后如何验证

这篇只讲 sqflite:怎么把它用稳、怎么把坑提前堵住。


1. sqflite 锁库是怎么来的(常见触发场景)

sqflite 底层是 SQLite。SQLite 同一时刻允许多个读,但写会有锁竞争;当应用层同时发起多次写入时,就有概率在某些机型/某些时序下撞上锁。

离线笔记最容易触发并发写的场景:

  • 编辑页自动保存(防抖) + 手动点击保存按钮同时触发写入
  • 返回页面兜底保存,和上一轮自动保存写入“时间撞车”
  • 列表页左滑删除触发写入,恰好编辑页也在写入

典型报错(示意):

DatabaseException(database is locked)

2. 解决思路:把所有写操作串行化(同一时刻只写一次)

很多人第一反应是“加大超时/重试”,但更稳的做法是:应用层写入队列

目标很简单:所有 insert/update/delete 统一排队执行,保证同一时刻只有一个写事务在跑。

这一步直接围绕 sqflite 的写接口做封装,不需要引入新库。


3. 实现写入队列:AppDatabase.write(…)

📌 文件:lib/features/note/data/app_database.dart

下面这个 write 方法就是“写入队列”的核心:

class AppDatabase {
  AppDatabase._();

  static final AppDatabase instance = AppDatabase._();

  Database? _db;
  Future<void> _writeChain = Future.value();

  Future<Database> get database async {
    final existing = _db;
    if (existing != null) return existing;
    final db = await _open();
    _db = db;
    return db;
  }

  Future<T> write<T>(Future<T> Function(Database db) action) {
    final future = _writeChain.then((_) async {
      final db = await database;
      return action(db);
    });
    _writeChain = future.then((_) {}, onError: (_) {});
    return future;
  }
}

这段代码做了什么:

  • _writeChain 是一条 Future 链
  • 每次写入都挂在链尾部,自动变成串行执行
  • 写失败不会把队列卡死(用 onError 把链继续往后走)

📌 这一步解决的核心问题:

  • 多处业务同时触发写入时,不会并发抢锁,而是排队一个个写

4. DAO 写操作统一走 write 队列(insert/update/delete)

📌 文件:lib/features/note/data/note_dao.dart

把写操作从 await _db.database 改成 _db.write(...)

Future<int> insert(Note note) async {
  return _db.write((db) => db.insert('notes', _toInsertRow(note)));
}

Future<int> update(Note note) async {
  final id = note.id;
  if (id == null) throw StateError('Cannot update note without id');
  return _db.write(
    (db) => db.update(
      'notes',
      _toUpdateRow(note),
      where: 'id = ?',
      whereArgs: [id],
    ),
  );
}

Future<int> softDelete(int id) async {
  return _db.write(
    (db) => db.update(
      'notes',
      {
        'is_deleted': 1,
        'updated_at': DateTime.now().millisecondsSinceEpoch,
      },
      where: 'id = ?',
      whereArgs: [id],
    ),
  );
}

这段改动的意义:

  • 所有写入入口被收口AppDatabase.write
  • UI/Repository 不需要关心“锁库/并发”细节
  • 后续要做事务(transaction)也有一个统一位置可加

5. 迁移怎么做:升级 DB version + onUpgrade

离线应用很难避免“表结构变化”。最常见的变化是加字段、加索引。

本例演示一个最安全的迁移:给 notes 表加一个 pinned 字段(置顶),默认 0。

📌 文件:lib/features/note/data/app_database.dart

return openDatabase(
  path,
  version: 2,
  onCreate: (db, version) async {
    await db.execute('''
CREATE TABLE notes(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL DEFAULT '',
  content TEXT NOT NULL DEFAULT '',
  pinned INTEGER NOT NULL DEFAULT 0,
  is_deleted INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
)
''');
  },
  onUpgrade: (db, oldVersion, newVersion) async {
    if (oldVersion < 2) {
      await db.execute(
        "ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0",
      );
    }
  },
);

迁移写法的要点:

  • version 必须递增(否则 onUpgrade 不会触发)
  • onUpgradeoldVersion 分段处理,保证可以从任意旧版本升级到新版本
  • ALTER TABLE ... ADD COLUMN 是 SQLite 比较稳妥的迁移方式之一(不会动旧数据)

6. 迁移后,模型与映射也要跟着改(否则读写会对不上)

📌 文件:lib/features/note/data/note.dart

class Note {
  final bool pinned;
  // ...
}

📌 文件:lib/features/note/data/note_dao.dart

Map<String, Object?> _toInsertRow(Note note) => {
  'pinned': note.pinned ? 1 : 0,
  // ...
};

Note _fromRow(Map<String, Object?> row) => Note(
  pinned: (row['pinned'] as int? ?? 0) != 0,
  // ...
);

这一步的意义:

  • 建表/迁移只是“数据库层面能存”,模型映射不改会导致“代码层面读写对不上”

7. 自测清单(Day3)

🧪 锁库/并发写:

  • 编辑页快速输入 + 立刻点保存 + 立刻返回,确认不会出现保存失败
  • 列表页快速连续删除,确认不会卡死

🧪 迁移:

  • 已有旧数据的情况下升级到新版本,确认旧笔记仍存在
  • 新建/编辑/删除流程正常

📷 在这里插入图片描述



---

## 8. 下一步(Day4 方向)

Day4 可以开始做“私密笔记”,核心第三方库:`flutter_secure_storage`(敏感字段不进明文数据库)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐