Flutter floor数据库类orm插件使用详解

3,267 阅读2分钟

最近Flutter项目有使用sqlite数据库的需求,研究了一下市面上多款插件,最终选择floor这款插件,选择的理由很简单,用起来方便,看起来简洁。

[floor pub地址](floor | Flutter Package (pub.dev))

常规使用

安装

在项目里的pubspec.yaml文件里添加

dependencies:
  floor: ^1.2.0

dev_dependencies:
  floor_generator: ^1.2.0
  build_runner: ^2.1.2

注意区分dependenciesdev_dependencies

创建表

新建一个类,类名上添加@entity注解,如下:

import 'package:floor/floor.dart';

@entity
class User {
  @PrimaryKey()
  int? id;
  
  String userId;
  
  // 表里字段为custom_name 不可为空
  @ColumnInfo(name: 'custom_name', nullable: false)
  String name;

  User(this.id, this.userId,this.name);
}

@PrimaryKey()表示该字段为自增key字段,或者在@entity中定义,使用大写,例:

@Entity(primaryKeys: ['id', 'name'])

创建Dao类

新建一个类,类名上添加@dao注解,如下:

import 'package:database_demo/database/entity/user_entity.dart';
import 'package:floor/floor.dart';

@dao
abstract class UserDao{
  @Query('SELECT * FROM User')
  Future<List<User>> findAllUser();

  /// 查询语句里的 userId 要和方法参数里的参数名一致
  @Query('SELECT * FROM User WHERE userId=:userId')
  Future<List<User>> findAllUserById(String userId);

  @insert
  Future<void> insertUser(User user);

  @update
  Future<int> updateUser(User user);

  @delete
  Future<int> deleteUser(User user);
}

创建数据库

创建一个继承FloorDatabase的抽象类。此外,还需要将@Database()注解添加到类上面。确保将创建的实体添加到@Database注释的entities属性中。 为了使生成的代码可以正常使用,还需要添加下面列出的import

// 必须的包
import 'dart:async';
import 'package:floor/floor.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart' as sqflite;

import 'package:database_demo/database/dao/user_dao.dart';
import 'package:database_demo/database/entity/user_entity.dart';

part 'database.g.dart';

// 执行命令 flutter pub run build_runner build --delete-conflicting-outputs
@Database(version: 1, entities: [User])
abstract class MyDataBase extends FloorDatabase{
  UserDao get userDao;
}

创建完成之后终端在当前项目下执行命令生成相关文件:

flutter pub run build_runner build --delete-conflicting-outputs

使用

import 'package:database_demo/database/database.dart';
import 'package:database_demo/database/entity/user_entity.dart';

class DataBaseManager {
  static Future<MyDataBase> database() async {
    // 关键
    // $FloorMyDataBase是生成的类 名称为$Floor+你创建的数据库类名字
    final database =
        await $FloorMyDataBase.databaseBuilder('app_database.db').build();
    return database;
  }

  /// 查询
  static Future<List<User>> queryAllUser() async {
    var myDataBase = await database();
    return myDataBase.userDao.findAllUser();
  }

  /// 根据Id查询
  static Future<List<User>> queryUserById(String userId) async {
    var myDataBase = await database();
    return myDataBase.userDao.findAllUserById(userId);
  }

  /// 插入
  static Future<void> insertUser(User user) async {
    var myDataBase = await database();
    return await myDataBase.userDao.insertUser(user);
  }

  /// 更新 返回[int] 表示受影响的行数
  static Future<int> updateUser(User user) async {
    var myDataBase = await database();
    return await myDataBase.userDao.updateUser(user);
  }

  /// 删除 返回[int] 表示受影响的行数
  static Future<int> deleteUser(User user) async {
    var myDataBase = await database();
    return await myDataBase.userDao.deleteUser(user);
  }
}

进阶操作

@Query

例:

@Query('SELECT * FROM Person WHERE id = :id')
Future<Person> findPersonById(int id);// 根据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(); // 查找多条数据

@Query('SELECT * FROM Person')
Stream<List<Person>> findAllPersonsAsStream(); // 流式返回

@Query('DELETE FROM Person')
Future<void> deleteAllPersons(); // 无返回值删除所有

@Query('SELECT * FROM Person WHERE id IN (:ids)')
Future<List<Person>> findPersonsWithIds(List<int> ids); // 使用IN子查询

在使用like操作符时,不能在查询本身中定义像%foo%这样的模式匹配参数。正确使用如下:

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

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

@insert

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

/// 冲突默认终止
@insert()
Future<void> insertUser(User user);

/// 大写指定冲突规则
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertUser(User user);

/// 可选择的冲突规则
enum OnConflictStrategy {
  /// 替换并继续
  replace,

  /// 回滚
  rollback,

  /// 中止
  abort,

  /// 使事务失败
  fail,

  /// 忽略冲突
  ignore,
}

@insert@Insert可以返回void、intList<int>的值。

  • void 无返回值
  • int 返回插入项目的主键
  • List<int> 返回插入项的主键

@Update

@update 将方法标记为更新方法。使用大写的@Update时,可以指定冲突策略。否则,它只是默认中止更新。这些方法可以返回voidint

  • void 无返回值
  • int 受影响的行数

@Delete

@Delete 将方法标记为删除方法。这些方法可以返回void或int。

  • void 无返回值
  • int 删除的行数

批量操作示例

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

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

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

使用Stream

查询不仅可以在调用时返回一次值,还可以返回连续的查询结果流。返回的流使与发生在数据库表上的更改保持同步。该特性与StreamBuilder非常配合。 这方法返回的是广播流。因此,它可以有多个监听器。 例:

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

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

Transactions

当您想在事务中执行某些操作时,必须向方法添加@transaction注解。还需要添加async修饰符。这些方法只能返回Future<void>。 例:

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

使用外键

向引用实体的@Entity注释添加一个ForeignKeys列表。childColumns定义当前实体的列,而parentColumns定义父实体的列。外键动作可以在为onUpdateonDelete属性定义它们之后被触发。例:

@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);
}

索引

@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);
}

忽略字段

默认情况下,实体的hashCode属性和所有静态字段都会被忽略,因此会从库的映射中排除。如果需要忽略其他字段,则应该使用@ignore注解,如下:

class Person {
  @primaryKey
  final int id;

  final String name;

  @ignore
  String nickname;

  Person(this.id, this.name);
}

迁移

无论何时对实体进行更改,都需要迁移旧数据。首先,更新的实体。接下来,增加数据库版本。定义一个Migration,它指定startVersionendVersion和一个执行SQL来迁移数据的函数。最后,在获得的数据库构建器上使用addMigrations()来添加迁移。不要忘记再次执行代码生成命令,以创建用于处理新实体的代码。 例:

// 新增nikeName字段
@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);
}

// 提升数据库版本
@Database(version: 2)
abstract class AppDatabase extends FloorDatabase {
  PersonDao get personDao;
}

// 创建一个migration
final migration1to2 = Migration(1, 2, (database) {
  database.execute('ALTER TABLE person ADD COLUMN nickname TEXT');
});

final database = await $FloorAppDatabase
    .databaseBuilder('app_database.db')
    .addMigrations([migration1to2])// 关键
    .build();

关键事件回调

final callback = Callback(
   onCreate: (database, version) { /* 已创建数据库 */ },
   onOpen: (database) { /* 数据库已打开*/ },
   onUpgrade: (database, startVersion, endVersion) { /* 数据库已升级 */ },
);

final database = await $FloorAppDatabase
    .databaseBuilder('app_database.db')
    .addCallback(callback) // 关键
    .build();

项目源码

HongZhiQing/detabase_demo (github.com)