本文正在参与「金石方案」
前言
前面两篇咱们介绍了运用 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
需求自己编写 fromJson
和 toJson
办法来完结数据库数据到实体类目标的转化。此外,遇到 SQLite 不支持的数据类型(如 DateTime
和 List<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
数据类型的转化,从而避免了每个实体类都要独自编写转化代码。比方咱们在备忘录用到了两个类型 DateTime
和 List<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
。:觉得有收获请点个赞鼓舞一下!
:收藏文章,方便回看哦!
:评论沟通,互相前进!
本文正在参与「金石方案」