本文正在参与「金石方案」

前言

前面两篇咱们介绍了运用 sqflite 办理 Flutter 本地 SQLite 数据库。运用 sqflite 相对来说还是有点杂乱,比方需求自己写数据库数据到实体类目标的转化,遇到数据不兼容的时分需求手动转化,增加了不少繁琐的代码。本篇咱们就来介绍一个 ORM 结构,来简化数据库的办理,这个结构便是 floor

floor 简介

floor 是根据 sqflite 的一个轻量级的 ORM 结构,经过注解和代码生成能够将数据库数据直接映射为实体类目标。floor 内置了许多操作数据库的办法,比方增删改查,让咱们快速接入数据库。一起,也能够在注解中编写 SQL来完结杂乱的数据库查询,比方 IN 查询、数据核算等等。经过注解和代码生成能够削减大量手写代码,提高咱们的开发功率和代码的可维护性。floor 的文档十分完善,大家能够到github阅读相关的文档:pinchbv.github.io/floor/getti…。
floor 需求引进的开发依赖如下,都是用于根据注解生成代码。

dev_dependencies:
    flutter_test:
        sdk: flutter
    # ...
    floor_generator: ^1.4.1
    build_runner: ^2.3.3

接下来咱们就以之前的备忘录为例,来看看运用 floor 后的改进。

ORM 映射

咱们之前的备忘录类 Memo 需求自己编写 fromJsontoJson 办法来完结数据库数据到实体类目标的转化。此外,遇到 SQLite 不支持的数据类型(如 DateTimeList<String>)时,还需求处理转化代码。咱们来看 floor 如何处理。
floor 将数据库操作分为实体类和 DAO,实体类与数据库的映射经过注解完结。例如咱们的 Memo 类,调整后的代码如下所示。

@entity
class Memo {
  @PrimaryKey(autoGenerate: true)
  final int? id;
  String title;
  String content;
  @ColumnInfo(name: 'created_time')
  DateTime createdTime;
  @ColumnInfo(name: 'modified_time')
  DateTime modifiedTime;
  List<String> tags;
  Memo({
    this.id,
    required this.title,
    required this.content,
    required this.createdTime,
    required this.modifiedTime,
    required this.tags,
  });
}

这儿阐明一下常见的注解:

  • @entity:表明这是一个实体类,会和数据库的某个数据表映射,默许表名便是类名。假如要手动指定表名,能够运用@Entity(tableName: tableName)经过 tableName 指定数据表称号。floor 会主动根据@entity 注解生成创立数据表的 SQL 语句。
  • @primaryKey:表明字段为主键,假如需求运用自增主键,能够运用@PrimaryKey(autoGenerate: true)
  • @ColumnInfo(name: name):设置实体类成员特点和数据表字段的映射关系,默许 floor 运用的数据表字段称号和类成员特点称号共同,假如需求指定数据表字段名,就能够运用这个注解。
  • @ignore:疏忽某个成员特点,即该特点不发生相应的数据表字段。注意,经过 get 办法发生的核算特点默许就会被疏忽,例如长方形面积 double get area => width * height

DAO 用于从数据库查询数据并转化为实体类目标,从数据库查询数据和转化的代码经过注解直接生成。DAO 提供了根底的刺进、更新和删去办法,这些办法能够经过注解@insert@update @delete完结,不需求编写 SQL。
一起,对于刺进和更新能够设置冲突战略,战略能够是间断(abort)、回滚(rollback)、替换(replace)、疏忽(ignore)、失败(fail)。其间除了替换以外,其他都是和数据库事务有关。

@dao
abstract class MemoDao {
  @Query('SELECT * FROM Memo ORDER BY modified_time DESC')
  Future<List<Memo>> findAllMemos();
  @Query(
      'SELECT * FROM Memo WHERE title LIKE :searchKey OR content LIKE :searchKey ORDER BY modified_time DESC')
  Future<List<Memo>> findMemoWithSearchKey(String searchKey);
  @Query('SELECT * FROM Memo WHERE id = :id')
  Stream<Memo?> findMemoById(int id);
  @insert
  Future<void> insertMemo(Memo memo);
  @Update(onConflict: OnConflictStrategy.replace)
  Future<void> updateMemo(Memo memo);
  @delete
  Future<void> deleteMemo(Memo memo);
}

转化器

运用 floor 能够统一 Dart 数据类型到 SQLite 字段的转化方式。经过界说不同的类型转化器TypeConverter完结数据库和Dart 数据类型的转化,从而避免了每个实体类都要独自编写转化代码。比方咱们在备忘录用到了两个类型 DateTimeList<String> 就界说了对应的转化器。

class StringListConverter extends TypeConverter<List<String>, String> {
  @override
  List<String> decode(String databaseValue) {
    return databaseValue.isNotEmpty ? databaseValue.split('|') : [];
  }
  @override
  String encode(List<String> value) {
    return value.join('|');
  }
}
class DateTimeConverter extends TypeConverter<DateTime, int> {
  @override
  DateTime decode(int databaseValue) {
    return DateTime.fromMillisecondsSinceEpoch(databaseValue);
  }
  @override
  int encode(DateTime value) {
    return value.millisecondsSinceEpoch;
  }
}

运用转化器只需求在界说数据库FloorDatabase 的抽象类的时分引进到注解@TypeConverters就能够了。

@TypeConverters([StringListConverter, DateTimeConverter])
@Database(version: 1, entities: [Memo])
abstract class MemoDatabase extends FloorDatabase {
  MemoDao get memoDao;
}

代码改造

一般来说 DAO 目标会在许多当地共用,合适运用单例方式来构造。这儿咱们在App启动的时分就运用 GetIt来完结MemoDao 的单例注册。

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final database =
      await $FloorMemoDatabase.databaseBuilder('app_database.db').build();
  final dao = database.memoDao;
  getIt.registerSingleton<MemoDao>(dao, signalsReady: true);
  runApp(const MyApp());
}

这儿调用ensureInitialized这个办法是保证 Flutter 和原生交互的部分已经完结,由于在 sqflite 中需求运用原生的文件存储。
备忘录列表的代码改造涉及数据操作的有两处,分别是列表刷新和删去备忘录。列表含糊查找时需求自己拼装含糊查找的字符,比方咱们这儿运用了百分号将查找关键词包裹完结恣意匹配。删去备忘录需求根据是否有查找调用不同的办法,这是由于对应的 SQL 不同。

void _refreshMemoList({String? searchKey}) async {
  List<Memo> memoList = searchKey == null
      ? await GetIt.I<MemoDao>().findAllMemos()
      : await GetIt.I<MemoDao>().findMemoWithSearchKey('%$searchKey%');
  setState(() {
    _memoList = memoList;
  });
}

删去就十分简单了,直接调用删去办法就好了。

void _deleteMemo(Memo memo) async {
    final confirmed = await _showDeleteConfirmationDialog(memo);
    if (confirmed != null && confirmed) {
      await GetIt.I<MemoDao>().deleteMemo(memo);
      _refreshMemoList();
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('已删去 "${memo.title}"'),
        duration: const Duration(seconds: 2),
      ));
    }
  }

添加备忘录的页面只需求更改保存备忘录的办法,并且由于不需求再对时间做转化,办法更为简练。

Future<void> _saveMemo(BuildContext context) async {
  var memo = Memo(
      title: _title,
      content: _content,
      createdTime: DateTime.now(),
      modifiedTime: DateTime.now(),
      tags: _tags);
  // 保存备忘录
  await GetIt.I<MemoDao>().insertMemo(memo);
}

编辑备忘录页面也类似,调用 updateMemo 办法即可完结保存。

Future<void> _saveMemo(BuildContext context) async {
  widget.memo.title = _title;
  widget.memo.content = _content;
  widget.memo.modifiedTime = DateTime.now();
  // 保存备忘录
  await GetIt.I<MemoDao>().updateMemo(widget.memo);
}

总结

代码已经提交到:本地存储相关代码,注意假如更改了 ORM 相关的类,需求运转下面的指令从头生成代码。

flutter packages pub run build_runner build

能够看到,经过 floor 这样的 ORM 结构能够让整个本地数据库办理的代码更为简练,复用性更高。假如说是本地数据存储比较杂乱的,引荐运用 ORM 结构来办理。

我是岛上码农,微信公众号同名。如有问题能够加自己微信沟通,微信号:island-coder

:觉得有收获请点个赞鼓舞一下!

:收藏文章,方便回看哦!

:评论沟通,互相前进!

本文正在参与「金石方案」