Flutter数据库管理插件sqflite_entities的使用
Flutter数据库管理插件sqflite_entities的使用
Sqflite包的扩展,旨在添加一些ORM能力。来自Sqlite/Sqflite爱好者,怀着爱的心。
索引
动机
Sqflite(https://pub.dev/packages/sqflite)是一个强大的sqlite包,支持很多功能,包括:
- 支持事务和批处理
- 在打开时自动管理版本
- 帮助插入/查询/更新/删除查询
- 在iOS和Android上在后台线程执行数据库操作
但缺少的是一个ORM方法,这将允许你通过类直接与数据库交互,并在代码中继续使用这些类的实例。
这个包是对Sqflite的一个尝试,添加了一些语法糖,使其在数据类型转换方面更加简单、声明式和安全。
假设我们有一个需要存储在数据库中的ProfileEntity类型的实体。在Dart中,它被声明如下:
class ProfileEntity {
final String firstName;
final String lastName;
final String? position;
final String? profile;
...
}
如果我们想要存储这种类型的实例到数据库(我们可以在后续部分将其称为“实体”),我们可以使用@SqlEntityDefinition
注解来标记它,并设置表名。
例如,它可以看起来像这样:
@SqlEntityDefinition(tableName: 'profile')
class ProfileEntity {
@SqlField(fieldName: 'first_name')
final String firstName;
@SqlField(fieldName: 'last_name')
final String lastName;
@SqlField(fieldName: 'position')
final String? position;
@SqlField(fieldName: 'profile')
final String? profile;
@SqlField(fieldName: 'team_name')
final String? teamName;
}
在这个例子中,使用@SqlEntityDefinition
注解,我们指定了表名(‘profile’)。使用@SqlField
注解,我们确定了哪些字段需要存储在数据库中,以及相应的列来存储它们。
这与Hive非常相似,不是吗?
代码生成
接下来是代码生成步骤。
我们在声明实体的文件中添加相应的部分文件,类似于我们为json_serializable、freezed或hive等所做的那样。
例如,
part 'profile_entity.sql.g.dart';
@SqlEntityDefinition(tableName: 'profile')
class ProfileEntity {
}
让我们开始代码生成:
flutter pub run build_runner build --delete-conflicting-outputs
作为结果,在代码生成之后,我们将获得以下函数:
存储实体:
await dbEngine.storeEntity(
ProfileEntity(
firstName: 'A',
lastName: 'B',
),
);
获取所有可用的实体:
await dbEngine.retrieveCollection<ProfileEntity>();
或者仅仅获取第一个实体:
await dbEngine.retrieveFirstEntity<ProfileEntity>();
根据给定条件更新实体:
final updatedEntity = ProfileEntity(
firstName: 'A',
lastName: 'C',
);
await dbEngine.updateEntity(
updatedEntity,
where: '${ProfileEntitySqlAdapter.columns.lastName} == ?',
whereArgs: ['B'],
);
或者我们可以根据条件删除某些实体:
await dbEngine.deleteEntity<ProfileEntity>(
where: '${ProfileEntitySqlAdapter.columns.lastName} == ?',
whereArgs: ['B'],
);
或者一次性删除所有实体:
await dbEngine.clearEntities<ProfileEntity>();
如果你需要根据自定义条件从数据库返回数据集,你可以使用queryEntities
构造:
final selectedEntities = await dbEngine.queryEntities<ProfileEntity>(
where: '${ProfileEntitySqlAdapter.columns.lastName} == ?',
whereArgs: ['B'],
orderBy: '${ProfileEntitySqlAdapter.columns.firstName} ASC',
limit: 1
);
一个非常重要的事情是事务的支持。
它们也是支持的,只需在帮助方法中指定事务对象:
示例:
await dbEngine.beginTransaction((txn) async {
await dbEngine.updateEntity(
updatedEntity,
where: '${ProfileEntitySqlAdapter.columns.lastName} == ?',
whereArgs: ['B'],
transaction: txn,
);
await dbEngine.deleteEntity<ProfileEntity>(
where: '${ProfileEntitySqlAdapter.columns.lastName} == ?',
whereArgs: ['B'],
transaction: txn,
);
});
还有批处理:
await dbEngine.beginTransaction(
(transaction) async {
final batch = transaction.batch();
dbEngine.storeEntitiesBatch(entities: entities, batch: batch);
await batch.commit(noResult: true);
},
)
内部包含什么?
在代码生成后,对于每个实体,在生成的类中,我们得到几个重要的东西:
- 工厂方法用于从原始SQL数据创建实体
- 工厂方法用于从带有
@SqlEntityDefinition
注解的类获取原始SQL数据 SqlAdapter
类 - 辅助类用于为带有@SqlEntityDefinition
注解的类创建表。 这个类包含了将实体序列化为数据库格式的逻辑(实际上是一个Json Map)、反序列化的逻辑,以及在数据库内创建表的Sqlite脚本。
对于上面的例子,我们的SqlAdapter
将如下所示:
class ProfileEntitySqlAdapter implements SqlAdapter<ProfileEntity> {
static const ProfileEntityColumnsDeclaration columns = ProfileEntityColumnsDeclaration();
const ProfileEntitySqlAdapter();
[@override](/user/override)
Type get modelType => ProfileEntity;
[@override](/user/override)
ProfileEntity deserialize(Map<String, dynamic> json) => _$ProfileEntityFromSqlDataMap(json);
[@override](/user/override)
Map<String, dynamic> serialize(ProfileEntity entity) => _$ProfileEntityToSqlDataMap(entity);
[@override](/user/override)
String get tableName => 'profile';
[@override](/user/override)
String get createEntityTableScript => '''
CREATE TABLE profile(
first_name TEXT NOT NULL ,
last_name TEXT NOT NULL ,
position TEXT,
profile TEXT,
team_name TEXT,
id INTEGER PRIMARY KEY AUTOINCREMENT ,
created INTEGER NOT NULL DEFAULT (DATETIME('now'))
)
''';
}
DbEngine
SqlAdapters
本身不知道如何与数据库交互。
它们需要某种引擎。
在上述示例中,我们通过dbEngine
实例与方法进行交互。
这是什么?
为了能够在数据库中为我们的实体创建适当的表,我们需要创建这样一个辅助类,继承自提供的SqliteEngine
类。
唯一需要实现的字段是要指定数据库版本(以便将来迁移)。
例如,
class ApplicationDBEngine extends SqliteEngine {
[@override](/user/override)
int get dbVersion => 1;
}
接下来,当我们想要与数据库交互时,我们需要创建并初始化我们的SqliteEngine
实例:
final dbEngine = ApplicationDBEngine();
使用registryAdapters
方法,我们枚举所有类型的sql适配器列表,以创建我们自己的表:
dbEngine.registryAdapters(
[
const ImageEntitySqlAdapter(),
const ProfileEntitySqlAdapter(),
],
);
最后我们需要做的是在数据库不存在的情况下创建数据库。
await dbEngine.initialize(databaseIdentity: 'test_only')
你也可以使用可选的filePathFactory
参数配置特定的文件名。
await dbEngine.initialize(
databaseIdentity: 'test_only',
filePathFactory: () => applicationDatabaseFilePath,
)
字段映射
在上面的Profile实体示例中,我们使用TEXT
数据类型来存储实体字段。
如果没有指定其他内容,默认情况下使用此数据类型。
但是,我们可以使用SqlField
注解上的fieldType
字段指定任何受支持的Sqlite数据类型。
例如,
@SqlField(
fieldName: 'uploaded_at',
fieldType: SqlFieldType.integer,
支持以下SqlFieldType
枚举类型:
- integer
- real
- text
- blob
这对应于使用的sqlite类型:https://www.sqlite.org/datatype3.html
如果我们的实体需要支持DateTime怎么办?
Sqlite不支持日期数据类型。
然而,使用Sqlfield
注解,我们可以支持将此数据类型自定义序列化/反序列化为任何受支持的Sqlite数据类型。
例如,通过microsecondsSinceEpoch
将datetime从dart转换为int数据类型。
反向通过DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch)
。
同样的规则也适用于布尔类型,将其转换为整数。
我们可以通过指定toRawData
、fromRawData
工厂的撕裂方法来实现这一点。
例如,
@SqlField(
fieldName: 'created_at',
fieldType: SqlFieldType.integer,
toRawData: SqliteCodec.dateTimeEncode,
fromRawData: SqliteCodec.dateTimeDecode,
)
final DateTime createdAt;
...
abstract class SqliteCodec {
static DateTime dateTimeDecode(int microsecondsSinceEpoch) => DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch);
static int dateTimeEncode(DateTime value) => value.microsecondsSinceEpoch;
}
主键
为了维护记录的唯一性,使用SqlField
注解,可以指定某个字段将用作相应表的主键。
例如,
@SqlField(
fieldName: 'id',
fieldType: SqlFieldType.integer,
isPrimaryKey: true
)
final int id;
你可以为多个字段指定isPrimaryKey
- 在这种情况下,将形成复合主键。
自定义字段
有时,对于某些实体,我们可能需要在数据库表中支持一些服务字段,这些字段不应成为类的一部分。
这可以是自增字段,或其他字段,如记录创建时间或更新时间。
在这种情况下,你可以使用@SqlEntityDefinition
注解,通过在实体之外指定这些字段。
例如,
@SqlEntityDefinition(
tableName: 'profile',
fields: [
SQLField(
fieldName: 'id',
fieldType: SqlFieldType.integer,
isAutoIncrement: true,
isPrimaryKey: true,
),
SQLField(
fieldName: 'created',
fieldType: SqlFieldType.integer,
defaultValueExpression: '(DATETIME(\'now\'))'),
],
)
对于上述情况,这些字段也可以在where表达式中使用,例如,用于自定义查询在queryEntities
中。
await dbEngine.queryEntities<ProfileEntity>(
where: '${ProfileEntitySqlAdapter.columns.created} > ? ',
whereArgs: [
lastCreatedDate,
],
);
从一个数据库版本迁移到新版本
上面,我们讨论了创建自己的SqliteEngine
类,
这将包含所有与实体交互的逻辑。
class ApplicationDBEngine extends SqliteEngine {
[@override](/user/override)
int get dbVersion => 1;
}
重要的是要指定版本号。
经常发生的是数据库发生变化,需要升级到新版本来支持。
也有对这种情况的支持,它是通过实现属性来实现的
Map<int, DatabaseMigration> get migrations;
migrations
getter应该返回一个Map,其键是版本号,值是由DatabaseMigration
函数指定的脚本。
例如,这样的迁移可能看起来像这样:
Map<int, DatabaseMigration> get migrations => {
2: (db) => db.execute(
'ALTER TABLE profile ADD COLUMN team_lead TEXT',
),
3: (db) => db.execute(
'ALTER TABLE image ADD COLUMN created INTEGER',
),
};
这个迁移集合告诉我们,如果客户端安装了版本2,而新数据库的当前版本应该是3,则只执行脚本:
ALTER TABLE image ADD COLUMN created INTEGER
如果之前安装的是版本1,则将执行两个迁移 - 从版本1到版本2,然后从版本2到版本3。
从Hive迁移到SQLite
如上所述,建议的方法与Hive非常相似,当我们使用注解来定义Hive将工作的类型。
@HiveType
注解对应于@SqlEntityDefinition
。
@HiveField
对应于@SqlField
。
TypeAdapter
负责与SqlAdapter
相同的功能。
同时,注册此类适配器列表的逻辑也非常相似。
比较代码:
hive.registerAdapter(ImageEntityAdapter());
hive.registerAdapter(ProfileEntityAdapter());
和
dbEngine.registryAdapters(
[
const ImageEntitySqlAdapter(),
const ProfileEntitySqlAdapter(),
],
);
因此,如果你想从Hive切换到Sqflite,你将很容易做到。 当然,这不会那么简单)。 你需要重构代码中所有与实体交互的部分(存储、检索等)。
但是,欢迎。
使用
要使用@Sql
…注解标记你的类,
并声明Sqlite数据库引擎类(底层是Sqflite),
你需要将sqflite_entities
包添加到pubspec.yaml
的依赖项中:
sqflite_entities
flutter pub add sqflite_entities
要执行代码生成,你需要将sqflite_entities_generator
包添加到dev_dependencies
中:
flutter pub add sqflite_entities_generator --dev
祝你好运,期待你的反馈。
完整示例Demo
下面是一个完整的示例Demo,展示了如何使用sqflite_entities插件来管理数据库。
import 'package:example/models/image_entity.dart';
import 'package:example/models/profile_entity.dart';
import 'package:example/sqlite/application_db_engine.dart';
import 'package:example/sqlite/sqlite_codec.dart';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Sqflite.setDebugModeOn(true);
dbEngine.registryAdapters(
[
const ImageEntitySqlAdapter(),
const ProfileEntitySqlAdapter(),
],
);
await dbEngine.initialize(
databaseIdentity: 'test_only',
);
await dbEngine.storeEntity(
ImageEntity(
id: 33,
width: 100,
height: 200,
createdAt: DateTime.now(),
isDeleted: false,
),
);
await dbEngine.retrieveCollection<ImageEntity>();
await dbEngine.queryEntities<ImageEntity>(
where: '${ImageEntitySqlAdapter.columns.id} = ? '
' AND ${ImageEntitySqlAdapter.columns.isDeleted} = ?',
whereArgs: [
33,
SqliteCodec.boolEncode(false),
],
);
await dbEngine.storeEntity(
ProfileEntity(
firstName: 'A',
lastName: 'B',
),
);
await dbEngine.retrieveCollection<ProfileEntity>();
runApp(const MyApp());
}
final dbEngine = ApplicationDBEngine();
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late Future<List<ProfileEntity>> profileEntitiesLoaderFuture;
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: profileEntitiesLoaderFuture,
initialData: const [],
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return ListView(
children: (snapshot.requireData as List<ProfileEntity>)
.map((e) => SizedBox(
height: 40,
child: Text('${e.firstName} ${e.lastName}'.toString()),
))
.toList(),
);
}
return const Center(child: CircularProgressIndicator());
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await dbEngine.storeEntity(
ProfileEntity(
firstName: 'John',
lastName: 'Smith',
),
);
fetchProfileEntities();
},
tooltip: 'Add profile',
child: const Icon(Icons.add),
),
);
}
void fetchProfileEntities() {
profileEntitiesLoaderFuture = dbEngine.retrieveCollection<ProfileEntity>();
setState(() {});
}
[@override](/user/override)
void initState() {
super.initState();
fetchProfileEntities();
}
}
更多关于Flutter数据库管理插件sqflite_entities的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter数据库管理插件sqflite_entities的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter项目中使用sqflite_entities
插件来管理SQLite数据库的示例代码。sqflite_entities
是一个基于sqflite
的ORM(对象关系映射)库,可以简化数据库操作。
1. 添加依赖
首先,你需要在pubspec.yaml
文件中添加sqflite_entities
依赖:
dependencies:
flutter:
sdk: flutter
sqflite_entities: ^x.y.z # 请替换为最新版本号
然后运行flutter pub get
来安装依赖。
2. 定义数据模型
创建一个数据模型类,并使用@Entity
注解来标记它。同时,使用@PrimaryKey
注解来指定主键字段。
import 'package:sqflite_entities/sqflite_entities.dart';
@Entity
class User {
@PrimaryKey(autoGenerate: true)
int? id;
@Column(notNull: true)
String name;
@Column(notNull: true)
int age;
User({required this.name, required this.age});
}
3. 配置数据库
在应用的入口文件(通常是main.dart
或app.dart
)中,配置数据库并生成DAO(数据访问对象)。
import 'package:flutter/material.dart';
import 'package:sqflite_entities/sqflite_entities.dart';
import 'package:sqflite_entities_generator/sqflite_entities_generator.dart';
import 'user_model.dart'; // 假设你的数据模型文件名为user_model.dart
part 'database.g.dart';
@Database(tables: [User])
abstract class AppDatabase extends Database {
AppDatabase() : super('my_database.db');
// 生成UserDao
UserDao get userDao;
}
@Dao(User)
abstract class UserDao {
@Query('SELECT * FROM User')
Future<List<User>> findAll();
@Insert
Future<void> insert(User user);
@Update
Future<void> update(User user);
@Delete
Future<void> delete(User user);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await $initSqliteEntities(); // 初始化数据库
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Sqflite Entities Demo'),
),
body: Center(
child: MyHomePage(),
),
),
);
}
}
4. 使用数据库
在你的页面或组件中,使用数据库进行CRUD操作。
import 'package:flutter/material.dart';
import 'database.dart';
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late AppDatabase _db;
@override
void initState() {
super.initState();
_db = AppDatabase();
_db.open().then((_) {
// 初始化数据
_insertSampleData();
});
}
Future<void> _insertSampleData() async {
await _db.userDao.insert(User(name: 'Alice', age: 30));
await _db.userDao.insert(User(name: 'Bob', age: 25));
}
Future<void> _fetchData() async {
List<User> users = await _db.userDao.findAll();
print(users);
// 在这里更新UI
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _fetchData,
child: Text('Fetch Data'),
),
],
);
}
}
5. 生成数据库代码
最后,你需要运行代码生成器来生成数据库和DAO相关的代码。在项目根目录下运行以下命令:
flutter pub run build_runner build --delete-conflicting-outputs
这将生成database.g.dart
文件,包含所有必要的数据库和DAO实现代码。
总结
以上是一个使用sqflite_entities
进行Flutter数据库管理的简单示例。通过定义数据模型、配置数据库、使用生成的DAO进行CRUD操作,你可以轻松地在Flutter应用中管理SQLite数据库。