本文地址:https://blog.csdn.net/qq_28550263/article/details/133377191
【介绍】:最近想找用于Dart和Flutter的ORM框架,偶然间发现了Floor,觉得还不错,做一些记录。
- 1. Floor 框架概述
- 1.1 框架简介
- 1.2 框架的构成
- 1.3 安装
- 1.4 架构体系
- 2. 实体(Entity)
- 支持的类型
- 主键
- 外键
- 索引
- 忽略字段
- 继承
- 数据库视图(Database Views)
- 3. 数据访问对象(DAO - Data Access Object)
- 4. 创建数据库
- 5. 运行代码生成器
- 6. 使用生成的代码
1. Floor 框架概述
1.1 框架简介
Floor 框架是一个用于在 Flutter 应用程序中使用 SQLite 数据库的开源库。它允许开发者轻松地将本地数据库集成到其应用程序中,并提供了一种类型安全、高效和易于使用的方法来执行数据库操作。
1.2 框架的构成
Floor 框架由多个关键模块组成,每个模块都有特定的功能,协同工作以实现在 Flutter 应用程序中使用 SQLite 数据库的目标。以下是 Floor 框架的主要模块以及它们的作用:
-
floor(运行时库):
- 作用:
floor
是 Floor 框架的运行时库,包含用于运行时数据库操作的类和方法。 - 关键类和方法:
FloorDatabase
:抽象类,表示数据库的根类,生成的数据库类需要扩展它。@Database
:用于标记生成的数据库类,指定数据库版本号、实体列表和其他配置信息。databaseBuilder
:用于创建数据库实例的方法。- 数据库操作方法:生成的数据库类中包含与数据库表的交互方法,例如插入、查询、更新等。
- 作用:
-
floor_generator(代码生成器):
- 作用:
floor_generator
是 Floor 框架的代码生成器模块,它会根据实体和 DAO 的定义生成数据库访问代码。 - 生成的代码: 该模块生成包括数据库类、DAO 类以及其他与数据库交互相关的代码,使开发者能够轻松执行数据库操作,而无需手动编写大量的重复代码。
- 作用:
-
floor_annotation(Floor 框架中的注解)
- 作用: floor_annotation是Floor 框架中的注解。这些注解用于标记和配置数据库实体(Entities)、数据访问对象(DAOs)以及数据库操作,以便 Floor 框架的代码生成器 floor_generator 可以根据这些注解生成相应的数据库访问代码。
- floor_annotation 提供了一种类型安全的方式来配置和定义数据库操作,并允许开发者在 Dart 代码中使用注解来描述数据库模型和操作,而无需手动编写大量的重复代码。
- 例如:
- @entity:用于标记 Dart 类,表示它是一个数据库实体(Entity)。实体类通常与数据库表相对应,用于定义表的结构和字段。
- @dao:用于标记抽象类或接口,表示它是数据访问对象(DAO)。DAO 类包含了与数据库表相关的操作方法,如插入、查询、更新和删除。
等等
- sqflite(数据库驱动,由floor自动调用):
- 作用: Floor 框架依赖于
sqflite
插件,它提供了在 Flutter 中与 SQLite 数据库进行底层交互的功能。 sqflite
插件负责管理 SQLite 数据库文件、执行原始 SQL 查询和处理数据库事务。
- 作用: Floor 框架依赖于
这些模块协同工作,使开发者能够轻松地在 Flutter 应用程序中集成和操作 SQLite 数据库。通过定义实体、DAO 和数据库,然后运行代码生成器,开发者可以获得类型安全、高效和易于维护的数据库访问代码,从而实现数据持久化和本地数据存储的需求。
- build_runner:build_runner 通常与 floor_generator 一起使用,以自动生成与数据库操作相关的 Dart 代码。
1.3 安装
flutter pub add floor
flutter pub add floor_generator build_runner --dev
其中:
floor
: 包含了将在应用程序中使用的所有代码。floor_generator
: 包含了生成数据库类的代码。build_runner
: 允许以具体的方式生成源代码文件。
将在你的yaml中看到:
dependencies:
flutter:
sdk: flutter
floor: ^1.4.0
dev_dependencies:
floor_generator: ^1.4.0
build_runner: ^2.1.2
版本可能随时间变化
1.4 架构体系
存储和访问数据的组件是 实体、数据访问对象(DAO) 和 数据库:
- 一个 实体 代表一个 持久类,也就是一个 数据库表;
- Dao管理对实体的访问,并负责内存中 对象 和表行 之间的 映射;
- 数据库是底层 SQLite 数据库的中心访问点。它 保存Dao,除此之外,还负责 初始化数据库及其 模式。
2. 实体(Entity)
一个实体是一个持久化类。Floor 自动创建内存中对象与数据库表行之间的映射。可以通过向 Entity 注释添加可选值来向 Floor 提供自定义元数据。它具有表名 tableName 的附加属性,该属性打开了为特定实体使用自定义名称的可能性,而不是使用类名。foreignKeys 允许向实体添加外键。有关如何使用这些内容的更多信息,请参阅外键部分。还支持索引。可以通过将索引添加到实体的 indices 值来使用它们。
@entity
注解用于标记了该类为持久类;@PrimaryKey
将类的属性标记为主键列。该属性必须是 int 类型。当启用autoGenerate
时,SQLite 可以自动生成该值;- 必须通过将
@primaryKey
注解添加到一个int
属性上为你的表添加一个主键,例如:// entity/person.dart import 'package: floor/floor.dart'; class Person { final int id; final String name; Person(this.id, this.name); }
@ColumnInfo
使单个表列的自定义映射变得可能。使用该注释,可以为列指定自定义名称。如果要将表的列设置为可空,请将实体字段标记为可空;- Floor 自动使用实体类中定义的第一个构造函数来从数据库行创建内存中对象
支持的类型
Floor 实体可以保存以下 Dart 类型的值,这些类型与它们对应的 SQLite 类型相互映射。
int
- INTEGERdouble
- REALString
- TEXTbool
- INTEGER(0 = false,1 = true)Uint8List
- BLOBenum
- INTEGER(按索引 0…n 记录)
如果要存储可以由上述类型之一表示的复杂 Dart 对象,请查看类型转换器。
主键
每当需要复合主键(例如 n-m 关系)时,设置主键的语法与前面提到的设置主键的方式不同。不是使用 @PrimaryKey
注释字段,而是使用 @Entity
注释的 primaryKey 属性。它接受一个由列名组成的列表,这些列名组成复合主键。
(primaryKeys: ['id', 'name'])
class Person {
final int id;
final String name;
Person(this.id, this.name);
}
外键
向引用实体的 Entity 注释添加 ForeignKeys 列表。childColumns 定义当前实体的列,而 parentColumns 定义父实体的列。在 onUpdate 和 onDelete 属性中定义外键操作后,可以触发外键操作。
// Dog 类,表示数据库中的狗实体
(
tableName: 'dog', // 指定表名为 'dog'
foreignKeys: [
ForeignKey(
childColumns: ['owner_id'], // 子列名为 'owner_id'
parentColumns: ['id'], // 父列名为 'id'
entity: Person, // 引用的实体为 Person 类
)
],
)
class Dog {
() // PrimaryKey 注解,将 id 标记为主键列
final int id;
final String name; // 名称
(name: 'owner_id') // ColumnInfo 注解,自定义列名为 'owner_id'
final int ownerId; // 所有者的 ID
// 构造函数,接受 id、name 和 ownerId 参数
Dog(this.id, this.name, this.ownerId);
}
@Entity
注解标记了Dog
类,指定了表名为 ‘dog’,同时指定了一个外键(foreign key),该外键将在 ‘owner_id’ 列上创建,它引用了另一个实体Person
。这意味着在数据库中将创建名为 ‘dog’ 的表,并在 ‘owner_id’ 列上创建一个外键,以确保数据的完整性。@PrimaryKey
注解标记了id
字段作为主键列,这意味着它在数据库中将用作主键。@ColumnInfo
注解标记了ownerId
字段,并自定义了列名为 ‘owner_id’,这允许在数据库表中使用不同的列名,而不是默认的字段名。- 构造函数接受
id
、name
和ownerId
参数,并用于创建Dog
实例。
这些注释和注释说明了 Dog
类的结构,以及如何配置表名、外键和自定义列名。
索引
索引有助于加速查询、连接和分组操作。有关 SQLite 索引的更多信息,请参阅官方文档。要在 floor 中创建索引,请将索引列表添加到 @Entity 注释。下面的示例演示了如何在实体的 custom_name 列上创建索引。
此外,可以使用其 name 属性命名索引。要将索引设置为唯一索引,请使用 unique 属性。
// Person 类,表示数据库中的个人实体
(tableName: 'person', indices: [Index(value: ['custom_name'])])
class Person {
// PrimaryKey 注解,将 id 标记为主键列
final int id;
(name: 'custom_name') // ColumnInfo 注解,自定义列名为 'custom_name'
final String name; // 姓名
// 构造函数,接受 id 和 name 参数
Person(this.id, this.name);
}
@Entity
注解标记了 Person 类,指定了表名为 ‘person’,同时指定了一个索引,该索引将在 ‘custom_name’ 列上创建。这意味着在数据库中将创建名为 ‘person’ 的表,并在 ‘custom_name’ 列上创建一个索引,以提高查询性能。@primaryKey
注解标记了 id 字段作为主键列,这意味着它在数据库中将用作主键。@ColumnInfo
注解标记了 name 字段,并自定义了列名为 ‘custom_name’,这允许在数据库表中使用不同的列名,而不是默认的字段名。
构造函数接受 id 和 name 参数,并用于创建 Person 实例。
忽略字段
默认情况下,实体的 getter、setter 和所有静态字段都会被忽略,并因此从库的映射中排除。如果要进一步忽略字段,应使用 @ignore 注释,并按以下示例中所示应用它。
// 人员类,表示数据库中的人员实体
class Person {
// PrimaryKey 注解,将 id 标记为主键列
final int id;
final String name; // 姓名
// Ignore 注解,指定 nickname 字段将被忽略
String nickname; // 昵称
// 默认情况下被忽略的字段,不会包含在库的映射中
String get combinedName => "$name ($nickname)";
// 构造函数,接受 id 和 name 参数
Person(this.id, this.name);
}
继承
与 Dao 一样,实体(和数据库视图)可以从共同的基类继承并使用它们的字段。实体只需扩展基类。这个构造将被视为如果基类的所有字段都是实体的一部分,这意味着数据库表将包含实体和基类的所有列。
基类不必为类单独注释。它的字段可以像正常的实体列一样进行注释。外键和索引必须在实体中声明,不能在基类中定义。
// 基础对象类,用于继承到其他实体
class BaseObject {
// 主键标记,指定 id 作为主键列
()
final int id;
// ColumnInfo 注解,用于自定义列名 create_time
(name: 'create_time')
final String createTime;
// ColumnInfo 注解,用于自定义列名 update_time
(name: 'update_time')
final String updateTime;
// 构造函数,接受 id、updateTime 和可选的 createTime 参数
BaseObject(
this.id,
this.updateTime, {
String createTime,
}) : this.createTime = createTime ?? DateTime.now().toString();
// 重写父类的 props 方法,通常用于数据比较
List<Object> get props => [];
}
// Comment 类,继承自 BaseObject 类
(tableName: 'comments') // Entity 注解,指定表名为 'comments'
class Comment extends BaseObject {
final String author; // 评论作者
final String content; // 评论内容
// 构造函数,接受 author、可选的 id、content、createTime 和 updateTime 参数
Comment(
this.author, {
int id,
this.content = '', // 默认为空字符串
String createTime,
String updateTime,
}) : super(id, updateTime, createTime: createTime); // 调用父类构造函数,传递 id、updateTime 和 createTime 参数
}
数据库视图(Database Views)
如果希望定义返回与实体不同类型的静态 SELECT 语句,最佳选择是使用 @DatabaseView
。数据库视图可以被理解为虚拟表,可以像真实表一样进行查询。
在 Floor 中,数据库视图的定义和使用方式与实体类似,主要区别在于只读访问,这意味着无法执行更新、插入和删除操作。与实体类似,如果未设置 viewName,则使用类名。
('SELECT distinct(name) AS name FROM person', viewName: 'name')
class Name {
final String name;
Name(this.name);
}
数据库视图不具有任何外键、主键或索引。相反,您应该手动定义与您的语句匹配的索引,并将它们放入涉及实体的 @Entity
注释中。
与实体类似,setter、getter 和静态字段将自动被忽略(与实体类似),您可以使用 @ignore
注解来指定要忽略的附加字段。
在代码中定义数据库视图后,您需要将它添加到 @Database
注释的 views 字段中:
(version: 1, entities: [Person], views: [Name])
abstract class AppDatabase extends FloorDatabase {
// DAO 获取器
}
然后,您可以通过 DAO 函数查询视图,就像查询实体一样。
数据库视图可以像实体一样从基类继承共同字段。
现在可以从 DAO 方法返回一个 Stream 对象,该方法查询数据库视图。但是,它将在整个数据库中的任何
@update
、@insert
、@delete
事件触发时发出,这可能会对运行时产生相当大的负担。请仅在明白自己在做什么时才添加它!这主要是因为检测哪些实体涉及到数据库视图的复杂性所致。
3. 数据访问对象(DAO - Data Access Object)
这个组件负责管理对底层的 SQLite 数据库的访问。抽象类包含了查询数据库的方法签名,这些方法必须返回 Future 或 Stream。
你可以通过将 @Query
注解添加到方法上来定义查询。SQL 语句必须放在括号中。方法必须返回你查询的实体的 Future 或 Stream。
@insert
标记了一个方法作为插入方法。
// dao/person_dao.dart
import 'package:floor/floor.dart';
abstract class PersonDao {
// 查询并返回所有的 Person 实体对象列表
('SELECT * FROM Person')
Future<List<Person>> findAllPersons();
// 根据提供的 id 查询并返回匹配的 Person 实体对象
// 这是一个异步数据流,当数据发生变化时,会自动更新
('SELECT * FROM Person WHERE id = :id')
Stream<Person?> findPersonById(int id);
// 将提供的 Person 对象插入到数据库中
// 这是一个异步操作,不返回任何数据
Future<void> insertPerson(Person person);
}
4. 创建数据库
它必须是一个继承自 FloorDatabase 的抽象类。此外,必须在类的签名中添加 @Database()
。确保将创建的实体添加到 @Database
注解的 entities 属性中。为了使生成的代码工作,还需要添加列出的导入项。
确保在这个文件的导入项下面添加 part ‘database.g.dart’;。重要的是要注意 ‘database’ 必须与数据库定义的文件名进行交换。在这种情况下,文件名命名为 database.dart。
// database.dart
import 'dart:async';
import 'package:floor/floor.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
import 'dao/person_dao.dart';
import 'entity/person.dart';
part 'database.g.dart'; // Floor代码生成器将在这里生成相关的代码
(version: 1, entities: [Person]) // 声明数据库版本号和包含的实体类
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao; // 获取PersonDao实例,用于执行数据库操作
}
5. 运行代码生成器
使用以下命令来运行生成器:
flutter packages pub run build_runner build
上面的命令用于在 Flutter 项目中运行代码生成工具以手动构建生成的代码。build命令 只会运行一次生成过程,然后退出。还可以在 Flutter 项目中使用 build_runner 模块的观察模式,以便在开发过程中自动更新生成的代码。如果想要在文件更改时自动运行它,请使用以下命令:
flutter packages pub run build_runner watch
6. 使用生成的代码
要获取数据库的实例,使用生成的 $FloorAppDatabase 类,它允许访问数据库生成器。名称由 $Floor 和数据库类名组成。传递给 databaseBuilder() 的字符串将是数据库文件的名称。要初始化数据库,调用 build() 并确保等待结果。
为了检索 PersonDao 实例,只需在数据库实例上调用 persoDao getter 即可。
可以参考下面的代码。
// 使用数据库生成器创建数据库实例,传递数据库文件的名称('app_database.db')
final database = await $FloorAppDatabase.databaseBuilder('app_database.db').build();
// 从数据库实例中获取 PersonDao 实例
final personDao = database.personDao;
// 创建一个 Person 对象
final person = Person(1, 'Frank');
// 将新的 Person 对象插入到数据库中
await personDao.insertPerson(person);
// 通过 ID 查找数据库中的 Person 记录
final result = await personDao.findPersonById(1);
// 输出查找到的 Person 记录
print('Found Person: $result');