floor flutter数据库操作库
github地址
Floor
为您的Flutter应用程序提供了一个简洁的SQLite抽象,其灵感来自于Room持久性库,它提供了内存中对象和数据库行之间的自动映射,同时仍然使用SQL提供对数据库的完全控制。因此,有必要了解SQL和SQLite,以便充分挖掘Floor的潜力。
类型安全
响应式
轻量级
SQL为中心的
没有隐藏的魔法 no hidden magic
没有隐藏成本 no hidden costs
支持iOS, Android, Linux, macOS, Winsdows
这个库正在迈向它的第一个稳定发行版!在集成了类型转换器和可嵌入对象之后,API表面在1.0之后才会改变。
快速入门
1.添加依赖
dependencies:
flutter:
sdk: flutter
floor: ^0.14.0
dev_dependencies:
floor_generator: ^0.14.0
build_runner: ^1.7.3
2.创建一个实体
它将表示一个数据库表以及业务对象的框架,
@entity
标记这一个类是对应一个数据库表。
@primaryKey
用于标记这是数据表的主键,需要是int属性
需要构造函数。
// entity/person.dart
import 'package:floor/floor.dart';
@entity
class Person {
@primaryKey
final int id;
final String name;
Person(this.id, this.name);
}
3.创建一个DAO(数据访问对象)
该组件负责管理对底层SQLite数据库的访问,抽象类包含查询数据库的方法签名,这些方法签名必须返回一个Future
或Stream
可以通过向方法中添加@Query
注释来定义查询,SQL语句必须添加到括号中,该方法必须返回您正在查询的实体的Future
或Stream
@insert
将方法标记为插入方法。
// dao/person_dao.dart
import 'package:floor/floor.dart';
@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future> findAllPersons();
@Query('SELECT * FROM Person WHERE id = :id')
Stream findPersonById(int id);
@insert
Future insertPerson(Person person);
}
4.创建数据库
它必须是一个继承FloorDatabase的抽象类,此外,需要将@Database()
添加到类的签名中,确保将2.创建一个实体
这一步创建的实体添加到@Database
的entities
注解属性中
为了使生成的代码工作,还需要导入所需要的类。
// database.dart
// required package imports
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'; // the generated code will be there
@Database(version: 1, entities: [Person])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}
注意:
1.确保添加部分‘database.g.dart’;在这个文件的导入下面,需要注意的是,‘database’必须与数据库定义的文件名进行交换。:在本例中,文件名为database.dart,所以part 'database.g.dart';
2.运行以下命令 flutter packages pub run build_runner build
,如果需要在文件变动时,自动运行命令使用flutter packages pub run build_runner watch
5.使用生成的代码
为了获得数据库的实例,使用生成的$FloorAppDatabase
类,它允许访问数据库构建器。该名称由$Floor
和数据库类名组成
传递给databaseBuilder()
的字符串将是数据库文件名。
要初始化数据库,请调用build()
并使用await
确保结果。
为了检索PersonDao
实例,在数据库实例上调用persoDao getter
就足够了。它的函数可以如下面的代码片段所示。
final database = await $FloorAppDatabase.databaseBuilder('app_database.db').build();
final personDao = database.personDao;
final person = Person(1, 'Frank');
await personDao.insertPerson(person);
final result = await personDao.findPersonById(1);
要了解更多示例,请查看example和floor_test目录。
架构
用于存储和访问数据的组件有 Entity, Data Access Object (DAO) and Database.
首先,Entity
:表示一个持久类,代表是一个数据库表
Data Access Object (DAO) 管理对实体的访问,并负责内存中对象和表行之间的映射
最后Database
是底层SQLite数据库的中央访问点,它操作dao,并负责初始化数据库及其模式, Room 是这个构图的灵感来源,因为它允许对组件的职责进行清晰的分离
该图显示了实体、DAO和数据库之间的关系。
实体对象
实体是一个持久类,Floor自动创建内存中对象和数据库表行之间的映射,通过向实体注解中添加可选值,可以向底层提供定制元数据。
它还有tableName的附加属性,可以为特定实体使用自定义名,而不是类名。
外键允许向实体添加外键。关于如何使用这些的更多信息可以在外键部分中找到,
也支持索引。它们可以通过向实体的索引值添加一个索引来使用。有关这些指标的进一步资料,请参阅指数部分。
@PrimaryKey将类的属性标记为主键列,此属性的类型必须是int,当启用自动生成时,SQLite可以自动生成该值,有关主键特别是复合主键的详细信息,请参考主键部分。
@ColumnInfo 启用单表列的自定义映射,使用注释,可以为列提供自定义名称,并定义列是否能够存储空值
Limitations
Floor自动使用entity类中定义的第一个构造函数从数据库行创建内存中的对象。
需要有一个构造函数。
@Entity(tableName: 'person')
class Person {
@PrimaryKey(autoGenerate: true)
final int id;
@ColumnInfo(name: 'custom_name', nullable: false)
final String name;
Person(this.id, this.name);
}
支持的类型
地板实体可以保存以下Dart类型的值,这些Dart类型映射到它们对应的SQLite类型,反之亦然
int
- INTEGERdouble
- REALString
- TEXTbool
- INTEGER (0 = false, 1 = true)Uint8List
- BLOB
Primary Keys
每当需要一个复合主键时(例如n-m关系),设置复合主键的语法与前面提到的设置主键的方法不同。使用的不是@PrimaryKey注释字段,而是@Entity注释的primaryKey属性。它接受组成复合主键的列名列表
@Entity(primaryKeys: ['id', 'name'])
class Person {
final int id;
final String name;
Person(this.id, this.name);
}
外键
ForeignKeys
用于向实体添加一个外键列表
childColumns
注解定义当前实体的列
parentColumns
定义父实体的列
在为onUpdate和onDelete属性定义外键操作之后,可以触发它们
@Entity(
tableName: 'dog',
foreignKeys: [
ForeignKey(
childColumns: ['owner_id'],
parentColumns: ['id'],
entity: Person,
)
],
)
class Dog {
@PrimaryKey()
final int id;
final String name;
@ColumnInfo(name: 'owner_id')
final int ownerId;
Dog(this.id, this.name, this.ownerId);
}
索引
索引有助于加快查询、联接和分组操作.有关SQLite索引的更多信息,请参阅官方文档
要使用floor创建索引,请向@Entity注解添加索引列表。下面的示例展示了如何在实体的custom_name列上创建索引
而且,索引可以通过使用其name属性来命名。若要将索引设置为唯一,请使用unique属性
@Entity(tableName: 'person', indices: [Index(value: ['custom_name'])])
class Person {
@primaryKey
final int id;
@ColumnInfo(name: 'custom_name', nullable: false)
final String name;
Person(this.id, this.name);
}
忽略字段
默认情况下,实体的getter、setter和所有静态字段都被忽略,因此被排除在库的映射之外。如果进一步的字段应该被忽略,应该使用和应用@ignore注释,如下面的代码片段所示
class Person {
@primaryKey
final int id;
final String name;
@ignore
String nickname;
// ignored by default
String get combinedName => "$name ($nickname)";
Person(this.id, this.name);
}
继承
与dao一样,实体(和数据库视图)可以从一个公共基类继承并使用它们的字段,实体只需要扩展基类,这个构造将被视为基类中的所有字段都是实体的一部分,这意味着数据库表将拥有该实体和基类的所有列
基类不必为类提供单独的注解,它的字段可以像普通的实体列一样进行注释。外键和索引必须在实体中声明,不能在基类中定义
class BaseObject {
@PrimaryKey()
final int id;
@ColumnInfo(name: 'create_time', nullable: false)
final String createTime;
@ColumnInfo(name: 'update_time')
final String updateTime;
BaseObject(
this.id,
this.updateTime, {
String createTime,
}) : this.createTime = createTime ?? DateTime.now().toString();
@override
List
数据库视图
如果您想定义静态选择语句,它返回的类型与您的实体不同,您最好的选择是使用@DatabaseView。一个数据库视图可以被理解为一个虚拟表,可以像查询真实的表一样查询。
floor中的数据库视图的定义和使用类似于实体,主要的区别是访问是只读的,这意味着更新,插入和删除功能是不可能的。与实体类似,如果没有设置viewName,则使用类名。
@DatabaseView('SELECT distinct(name) AS name FROM person', viewName: 'name')
class Name {
final String name;
Name(this.name);
}
数据库视图没有任何外部/主键或索引。相反,您应该手动定义适合您的语句的索引,并将它们放入所涉及实体的@Entity注释中
setter、getter和静态字段会被自动忽略(与实体一样),您可以通过使用@ignore注释其他字段来指定要忽略的字段。
在代码中定义数据库视图之后,您必须通过将其添加到@Database注释的views字段来将其添加到数据库
@Database(version: 1, entities: [Person], views: [Name])
abstract class AppDatabase extends FloorDatabase {
// DAO getters
}
然后,您可以像实体一样通过DAO函数查询视图。
DatabaseViews可以从基类继承公共字段,就像在实体中一样。
局限性
现在可以从查询数据库视图的DAO方法返回Stream对象,
但是它会整个数据库中的任何@update, @insert, @delete事件中触发,这会对运行时造成很大的负担,只在你知道你在做什么时添加它!这主要是由于检测数据库视图中涉及哪些实体的复杂性。
数据访问对象
这些组件负责管理对底层SQLite数据库的访问,并被定义为具有方法签名和查询语句的抽象类。DAO类可以通过在使用mixin的同时实现和扩展类来使用继承的方法。
@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future> findAllPersons();
@Query('SELECT * FROM Person WHERE id = :id')
Stream findPersonById(int id);
@insert
Future insertPerson(Person person);
}
查询
通过向方法签名添加带有括号中的查询的@Query()注释,方法签名转换为查询方法,请耐心等待SQL语句的正确性。在生成代码时,只对它们进行了部分验证。这些查询必须返回实体或空的Future或Stream。当您想删除表的完整内容时,返回Future
@Query('SELECT * FROM Person WHERE id = :id')
Future findPersonById(int id);
@Query('SELECT * FROM Person WHERE id = :id AND name = :name')
Future findPersonByIdAndName(int id, String name);
@Query('SELECT * FROM Person')
Future> findAllPersons(); // select multiple items
@Query('SELECT * FROM Person')
Stream> findAllPersonsAsStream(); // stream return
@Query('DELETE FROM Person')
Future deleteAllPersons(); // query without returning an entity
@Query('SELECT * FROM Person WHERE id IN (:ids)')
Future> findPersonsWithIds(List ids);
在使用SQLite的LIKE操作符时,查询参数必须由方法的输入提供,不能在查询本身中定义像%foo%这样的模式匹配参数。
// dao
@Query('SELECT * FROM Person WHERE name LIKE :name')
Future> findPersonsWithNamesLike(String name);
// usage
final name = '%foo%';
await dao.findPersonsWithNamesLike(name);
数据更改
使用@insert、@update和@delete注解插入和更改持久数据,所有这些方法都接受单个或多个实体实例。
@insert将方法标记为插入方法,当使用大写的@Insert时,您可以指定一个冲突策略,否则,它将默认终止插入。
这些方法可以返回void、int或List
void
return nothingint
return primary key of inserted itemList
return primary keys of inserted items
@update将方法标记为更新方法,当使用大写的@Update时,您可以指定一个冲突策略,否则,它将默认中止更新,:这些方法可以返回void或int的Future
void
return nothing-
int
return number of changed rows@delete将一个方法标记为删除方法,这些方法可以返回void或int的Future
void
return nothingint
return number of deleted rows
// examples of changing multiple items with return
@insert
Future> insertPersons(List person);
@update
Future updatePersons(List person);
@delete
Future deletePersons(List person);
数据流
返回的流使您与数据库表中发生的更改保持同步,返回的流使您与数据库表中发生的更改保持同步。这个特性在StreamBuilder小部件上很好地发挥作用,它接受一个值流,并在出现新的发射时重新构建自己。
这些方法返回广播流,因此可以有多个侦听器。
// definition
@Query('SELECT * FROM Person')
Stream> findAllPersonsAsStream();
// usage
StreamBuilder>(
stream: dao.findAllPersonsAsStream(),
builder: (BuildContext context, AsyncSnapshot> snapshot) {
// do something with the values here
},
);
局限性
只有注解了@insert、@update和@delete的方法才会触发流排放。通过使用@Query()注释插入数据则不需要
现在,如果函数查询数据库视图,则可以返回一个流。但是它会触发整个数据库中的任何@update、@insert、@delete事件,这会对运行时造成相当大的负担,如果你知道你在做什么,请添加它!这主要是由于检测数据库视图中涉及哪些实体的复杂性
当没有查询结果时,返回单个项流(如stream
事务
每当您想在事务中执行某些操作时,您就必须向方法中添加@transaction注解。
@transaction
Future replacePersons(List persons) async {
await deleteAllPersons();
await insertPersons(persons);
}
继承
数据访问对象类支持继承,如下所示,继承级别没有限制,因此每个抽象父对象可以有另一个抽象父对象。请记住,只有抽象类允许没有实现主体的方法签名,因此确保将要继承的方法定位在一个抽象类中,并使用DAO扩展这个类
@dao
abstract class PersonDao extends AbstractDao {
@Query('SELECT * FROM Person WHERE id = :id')
Future findPersonById(int id);
}
abstract class AbstractDao {
@insert
Future insertItem(T item);
}
// usage
final person = Person(1, 'Simon');
await personDao.insertItem(person);
final result = await personDao.findPersonById(1);
Migrations
在对实体进行更改时,还需要迁移旧数据。首先,更新您的实体。接下来,增加数据库版本。定义一个迁移,指定startVersion、endVersion和一个执行SQL来迁移数据的函数,最后,在获得的数据库构建器上使用addMigrations()来添加迁移,不要忘记再次触发代码生成器,以创建用于处理新实体的代码。
// update entity with new 'nickname' field
@Entity(tableName: 'person')
class Person {
@PrimaryKey(autoGenerate: true)
final int id;
@ColumnInfo(name: 'custom_name', nullable: false)
final String name;
final String nickname;
Person(this.id, this.name, this.nickname);
}
// bump up database version
@Database(version: 2)
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}
// create migration
final migration1to2 = Migration(1, 2, (database) async {
await database.execute('ALTER TABLE person ADD COLUMN nickname TEXT');
});
final database = await $FloorAppDatabase
.databaseBuilder('app_database.db')
.addMigrations([migration1to2])
.build();
内存数据库
要实例化内存中的数据库,请使用生成的$FloorAppDatabase类的静态inMemoryDatabaseBuilder()方法,而不是databaseBuilder()
final database = await $FloorAppDatabase.inMemoryDatabaseBuilder().build();
平台支持
Floor支持iOS、Android、Linux、macOS和Windows,
iOS和Android上的SQLite数据库访问是由sqflite提供的,而Linux、macOS和Windows使用sqflite的ffi实现。
目前还没有对web的Flutter的支持。