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)

同样的规则也适用于布尔类型,将其转换为整数。

我们可以通过指定toRawDatafromRawData工厂的撕裂方法来实现这一点。

例如,

  @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

1 回复

更多关于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.dartapp.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数据库。

回到顶部