floor flutter数据库操作库

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数据库的访问,抽象类包含查询数据库的方法签名,这些方法签名必须返回一个FutureStream

可以通过向方法中添加@Query注释来定义查询,SQL语句必须添加到括号中,该方法必须返回您正在查询的实体的FutureStream

@insert将方法标记为插入方法。

// dao/person_dao.dart

 import 'package:floor/floor.dart';

 @dao
 abstract class PersonDao {
   @Query('SELECT * FROM Person')
   Future<List<Person>> findAllPersons();

   @Query('SELECT * FROM Person WHERE id = :id')
   Stream<Person> findPersonById(int id);

   @insert
   Future<void> insertPerson(Person person);
 }

4.创建数据库

它必须是一个继承FloorDatabase的抽象类,此外,需要将@Database()添加到类的签名中,确保将2.创建一个实体这一步创建的实体添加到@Databaseentities注解属性中

为了使生成的代码工作,还需要导入所需要的类。

 // 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);

要了解更多示例,请查看examplefloor_test目录。

架构

用于存储和访问数据的组件有 Entity, Data Access Object (DAO) and Database.

首先,Entity表示一个持久类,代表是一个数据库表

Data Access Object (DAO) 管理对实体的访问,并负责内存中对象和表行之间的映射

最后Database是底层SQLite数据库的中央访问点,它操作dao,并负责初始化数据库及其模式, Room 是这个构图的灵感来源,因为它允许对组件的职责进行清晰的分离

该图显示了实体、DAO和数据库之间的关系。

floor-architecture.png

实体对象

实体是一个持久类,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 - INTEGER

  • double - REAL

  • String - TEXT

  • bool - 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<Object> get props => [];
}

@Entity(tableName: 'comments')
class Comment extends BaseObject {
  final String author;

  final String content;

  Comment(this.author,
      {int id, this.content = '', String createTime, String updateTime})
      : super(id, updateTime, createTime: createTime);
}

数据库视图

如果您想定义静态选择语句,它返回的类型与您的实体不同,您最好的选择是使用@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<List<Person>> findAllPersons();

  @Query('SELECT * FROM Person WHERE id = :id')
  Stream<Person> findPersonById(int id);

  @insert
  Future<void> insertPerson(Person person);
}
查询

通过向方法签名添加带有括号中的查询的@Query()注释,方法签名转换为查询方法,请耐心等待SQL语句的正确性。在生成代码时,只对它们进行了部分验证。这些查询必须返回实体或空的Future或Stream。当您想删除表的完整内容时,返回Future<void>很方便,下面是一些查询方法示例。

@Query('SELECT * FROM Person WHERE id = :id')
Future<Person> findPersonById(int id);

@Query('SELECT * FROM Person WHERE id = :id AND name = :name')
Future<Person> findPersonByIdAndName(int id, String name);

@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons(); // select multiple items

@Query('SELECT * FROM Person')
Stream<List<Person>> findAllPersonsAsStream(); // stream return

@Query('DELETE FROM Person')
Future<void> deleteAllPersons(); // query without returning an entity

@Query('SELECT * FROM Person WHERE id IN (:ids)')
Future<List<Person>> findPersonsWithIds(List<int> ids); 

在使用SQLite的LIKE操作符时,查询参数必须由方法的输入提供,不能在查询本身中定义像%foo%这样的模式匹配参数。

// dao
@Query('SELECT * FROM Person WHERE name LIKE :name')
Future<List<Person>> findPersonsWithNamesLike(String name);

// usage
final name = '%foo%';
await dao.findPersonsWithNamesLike(name);

数据更改

使用@insert、@update和@delete注解插入和更改持久数据,所有这些方法都接受单个或多个实体实例。

@insert将方法标记为插入方法,当使用大写的@Insert时,您可以指定一个冲突策略,否则,它将默认终止插入。

这些方法可以返回void、int或List<int>的Future

  • void return nothing

  • int return primary key of inserted item

  • List<int> 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 nothing

  • int return number of deleted rows

// examples of changing multiple items with return

@insert
Future<List<int>> insertPersons(List<Person> person);

@update
Future<int> updatePersons(List<Person> person);

@delete
Future<int> deletePersons(List<Person> person);

数据流

返回的流使您与数据库表中发生的更改保持同步,返回的流使您与数据库表中发生的更改保持同步。这个特性在StreamBuilder小部件上很好地发挥作用,它接受一个值流,并在出现新的发射时重新构建自己。

这些方法返回广播流,因此可以有多个侦听器。

// definition
@Query('SELECT * FROM Person')
Stream<List<Person>> findAllPersonsAsStream();

// usage
StreamBuilder<List<Person>>(
  stream: dao.findAllPersonsAsStream(),
  builder: (BuildContext context, AsyncSnapshot<List<Person>> snapshot) {
    // do something with the values here
  },
);

局限性

只有注解了@insert、@update和@delete的方法才会触发流排放。通过使用@Query()注释插入数据则不需要

现在,如果函数查询数据库视图,则可以返回一个流。但是它会触发整个数据库中的任何@update、@insert、@delete事件,这会对运行时造成相当大的负担,如果你知道你在做什么,请添加它!这主要是由于检测数据库视图中涉及哪些实体的复杂性

当没有查询结果时,返回单个项流(如stream <Person>)的函数不会发出

事务

每当您想在事务中执行某些操作时,您就必须向方法中添加@transaction注解。

@transaction
Future<void> replacePersons(List<Person> persons) async {
  await deleteAllPersons();
  await insertPersons(persons);
}

继承

数据访问对象类支持继承,如下所示,继承级别没有限制,因此每个抽象父对象可以有另一个抽象父对象。请记住,只有抽象类允许没有实现主体的方法签名,因此确保将要继承的方法定位在一个抽象类中,并使用DAO扩展这个类

@dao
abstract class PersonDao extends AbstractDao<Person> {
  @Query('SELECT * FROM Person WHERE id = :id')
  Future<Person> findPersonById(int id);
}

abstract class AbstractDao<T> {
  @insert
  Future<void> 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的支持。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345