[Flutter] floor 数据库你不知道的那些事

3,641 阅读3分钟

PS : 博主用Flutter写的玩安卓:https://github.com/codingmancui/flutter_frolo 欢迎star

Floor 数据库简介

Floor库是Flutter 应用的SQLite抽象支持。Floor库提供了轻量的SQLite抽象,在内存对象与数据库之间自动映射,同时可通过SQL完全控制数据库。

Floor 快速入门

1.添加依赖

dependencies:
   flutter:
     sdk: flutter
  floor: ^0.17.0

 dev_dependencies:
   floor_generator: ^0.17.0
   build_runner: ^1.10.3

2.创建一个实体

它将表示一个数据库表以及业务对象的框架, @Entity 标记这一个类是对应一个数据库表,可以指定表名,如果不指定默认表名实体名。 @primaryKey 用于标记这是数据表的主键,需要是int属性。 @ignore 用于忽略字段

@Entity(tableName:'history')
class Article {
  @primaryKey
  int id;
  List<Tags> tags;
  String title;
  @ignore
  int itemType = 0;
  Article(
      {this.id,
      this.tags,
      this.title,
      this.itemType});
}

3.创建一个DAO(数据访问对象)

该组件负责管理对底层SQLite数据库的访问,抽象类包含查询数据库的方法签名,这些方法签名必须返回一个Future或Stream

可以通过向方法中添加@Query注释来定义查询,该方法必须返回您正在查询的实体的Future或Stream @insert将方法标记为插入方法。

import 'package:floor/floor.dart';
import 'package:frolo/data/protocol/models.dart';

@dao
abstract class ArticleDao{
  @Query('SELECT * FROM HISTORY')
  Future<List<Article>> findAllArticles();

  @insert
  Future<void> insertArticle(Article article);
}

4.创建数据库

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

import 'dart:async';
// required package imports
import 'package:floor/floor.dart';
import 'package:sqflite/sqflite.dart' as sqflite;

import 'package:frolo/data/db/article_dao.dart';
import 'package:frolo/data/protocol/models.dart';

import 'article_tag_converter.dart';
part 'database.g.dart'; // the generated code will be there

@TypeConverters([ArticleTagConverter])
@Database(version: 1, entities: [Article])
abstract class WanAndroidDatabase extends FloorDatabase{
  ArticleDao get articleDao;
}

注意:

1.确保添加部分part '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.使用生成的代码

为了获得数据库的实例,使用生成的$FloorWanAndroidDatabase类,它允许访问数据库构建器。该名称由$Floor和数据库类名组成传递给databaseBuilder()的字符串将是数据库文件名。 要初始化数据库,请调用build()并使用await确保结果。

 final database = await $FloorWanAndroidDatabase.databaseBuilder('wan_android_database.db').build();
 final articleDao = database.articleDao;

 await articleDao.insertArticle(article);
 final result = await articleDao.findAllArticles();

Floor 采坑记

Entity类型存储

1. TypeConverters 类型

由于SQLite只允许存储少数类型的值。当需要存储更复杂的Dart在内存中的对象时,有时需要在Dart和SQLite兼容类型之间进行转换。

在上面的demo中,实体类Articletags字段为List<Tags>类型,我们如果如果不使用TypeConverters类型,在执行 flutter packages pub run build_runner build时便会报错,列表不支持<Tags*>*列类型

Column type is not supported for List<Tags*>*.
package:frolo/data/protocol/models.dart:63:1463 │   List<Tags> tags;
   │              ^^^^

2. TypeConverters 实现和用法

  1. 创建实现抽象TypeConverter的转换器类,并将内存中的对象类型和数据库类型作为参数化类型提供。这个类继承decode()encode()函数,它们定义从一种类型到另一种类型的转换。在Demo中将List<Tags>类型转化为String进行数据库存储
class ArticleTagConverter extends TypeConverter<List<Tags>, String> {
  @override
  List<Tags> decode(String databaseValue) {
    List list = json.decode(databaseValue);
    List<Tags> tags = new List();
    list.map((value) {
      tags.add(Tags.fromJson(value));
    });
    return tags;
  }

  @override
  String encode(List<Tags> value) {
    String v = json.encode(value);
    return v;
  }
}
  1. 通过使用@TypeConverters注释将创建的类型转换器应用到数据库,并确保在这里额外导入类型转换器的文件。在数据库文件中导入它总是必要的,因为生成的代码将是数据库文件的一部分,并且这是类型转换器实例化的位置。
@TypeConverters([ArticleTagConverter])
@Database(version: 1, entities: [Article])
abstract class WanAndroidDatabase extends FloorDatabase{
  ArticleDao get articleDao;
}

生成文件database.g.dart解析

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'database.dart';

// **************************************************************************
// FloorGenerator
// **************************************************************************

class $FloorWanAndroidDatabase {
  /// Creates a database builder for a persistent database.
  /// Once a database is built, you should keep a reference to it and re-use it.
  static _$WanAndroidDatabaseBuilder databaseBuilder(String name) =>
      _$WanAndroidDatabaseBuilder(name);

  /// Creates a database builder for an in memory database.
  /// Information stored in an in memory database disappears when the process is killed.
  /// Once a database is built, you should keep a reference to it and re-use it.
  static _$WanAndroidDatabaseBuilder inMemoryDatabaseBuilder() =>
      _$WanAndroidDatabaseBuilder(null);
}

class _$WanAndroidDatabaseBuilder {
  _$WanAndroidDatabaseBuilder(this.name);

  final String name;

  final List<Migration> _migrations = [];

  Callback _callback;

  /// Adds migrations to the builder.
  _$WanAndroidDatabaseBuilder addMigrations(List<Migration> migrations) {
    _migrations.addAll(migrations);
    return this;
  }

  /// Adds a database [Callback] to the builder.
  _$WanAndroidDatabaseBuilder addCallback(Callback callback) {
    _callback = callback;
    return this;
  }

  /// Creates the database and initializes it.
  Future<WanAndroidDatabase> build() async {
    final path = name != null
        ? await sqfliteDatabaseFactory.getDatabasePath(name)
        : ':memory:';
    final database = _$WanAndroidDatabase();
    database.database = await database.open(
      path,
      _migrations,
      _callback,
    );
    return database;
  }
}

class _$WanAndroidDatabase extends WanAndroidDatabase {
  _$WanAndroidDatabase([StreamController<String> listener]) {
    changeListener = listener ?? StreamController<String>.broadcast();
  }

  ArticleDao _articleDaoInstance;

  Future<sqflite.Database> open(String path, List<Migration> migrations,
      [Callback callback]) async {
    final databaseOptions = sqflite.OpenDatabaseOptions(
      version: 1,//版本号
      onConfigure: (database) async {
        await database.execute('PRAGMA foreign_keys = ON');
      },
      onOpen: (database) async {
        await callback?.onOpen?.call(database);
      },
      // 数据库升级
      onUpgrade: (database, startVersion, endVersion) async {
        await MigrationAdapter.runMigrations(
            database, startVersion, endVersion, migrations);

        await callback?.onUpgrade?.call(database, startVersion, endVersion);
      },
      // 数据库创建
      onCreate: (database, version) async {
        await database.execute(
            'CREATE TABLE IF NOT EXISTS `history` (`id` INTEGER, `apkLink` TEXT, `audit` INTEGER, `author` TEXT, `canEdit` INTEGER, `chapterId` INTEGER, `chapterName` TEXT, `collect` INTEGER, `courseId` INTEGER, `desc` TEXT, `descMd` TEXT, `envelopePic` TEXT, `fresh` INTEGER, `link` TEXT, `niceDate` TEXT, `niceShareDate` TEXT, `origin` TEXT, `prefix` TEXT, `projectLink` TEXT, `publishTime` INTEGER, `realSuperChapterId` INTEGER, `selfVisible` INTEGER, `shareDate` INTEGER, `shareUser` TEXT, `superChapterId` INTEGER, `superChapterName` TEXT, `tags` TEXT, `title` TEXT, `type` INTEGER, `userId` INTEGER, `visible` INTEGER, `zan` INTEGER, PRIMARY KEY (`id`))');

        await callback?.onCreate?.call(database, version);
      },
    );
    return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
  }

  @override
  ArticleDao get articleDao {
    return _articleDaoInstance ??= _$ArticleDao(database, changeListener);
  }
}

class _$ArticleDao extends ArticleDao {
  _$ArticleDao(this.database, this.changeListener)
      : _queryAdapter = QueryAdapter(database),
      //1 数据库写入,如果字段使用转换器则调用转换器encode方法
        _articleInsertionAdapter = InsertionAdapter(
            database,
            'history',
            (Article item) => <String, dynamic>{
                  'id': item.id,
                  'tags': _articleTagConverter.encode(item.tags),
                  'title': item.title,
                });

  final sqflite.DatabaseExecutor database;

  final StreamController<String> changeListener;

  final QueryAdapter _queryAdapter;

  final InsertionAdapter<Article> _articleInsertionAdapter;

  @override
  Future<List<Article>> findAllArticles() async {//数据库读取,该方法默认返回空对象,需要手动实现
    return _queryAdapter.queryList('SELECT * FROM history',
        mapper: (Map<String, dynamic> row) => Article(
              id: row['id'] as int,
              tags: _articleTagConverter.decode(row['tags']),
              title: row['title'] as String,
            ));
  }

  @override
  Future<void> insertArticle(Article article) async {
    await _articleInsertionAdapter.insert(article, OnConflictStrategy.replace);//可以自定primaryKey冲突策略
  }
}

// ignore_for_file: unused_element 转换器创建
final _articleTagConverter = ArticleTagConverter();

  1. 这里需要注意的是,由于该文件是自动创建,有些是需要手动修改的,在Demo中博主就遇到这个坑,查询到的数据全是空对象,最终问题是自动生成的代码中findAllArticles方法如下:查询到的row转换为Article时实际创建的是空对象
  @override
  Future<List<Article>> findAllArticles() async {//数据库读取
    return _queryAdapter.queryList('SELECT * FROM history',
        mapper: (Map<String, dynamic> row) => Article());
  }
  1. 在数据库insert过程中如果primaryKey冲突,我们可以指定冲突策略,一共有五种策略,这里Demo中使用的是OnConflictStrategy.replace。这里是不是很眼熟,比较像线程池中的拒绝策略
  @override
  Future<void> insertArticle(Article article) async {
    await _articleInsertionAdapter.insert(article, OnConflictStrategy.replace);
  }

PS : 博主用Flutter写的玩安卓:https://github.com/codingmancui/flutter_frolo 欢迎star