Froom 是 Flutter 里的 Room 吗
爱来自 wlz 学长:wilinz/froom
Froom 是 Flutter 里的 Room 吗
Section titled “Froom 是 Flutter 里的 Room 吗”如果之前写过安卓原生,那大概都见过 Room。
它的作用很简单:把 SQLite 包一层,让我们不用天天对着一吨字符串和游标发呆。虽然本质上还是数据库,但至少写起来像是在操作对象了。
那到了 Flutter 这边,有没有类似的东西呢?
有的兄弟,有的,它叫 Froom。目前由 21 级 APP lz 学长维护
先说它是干嘛的
Section titled “先说它是干嘛的”Froom 是一个给 Flutter 用的 SQLite 抽象库,前身叫做 Floor,灵感来源于安卓原生的 Room。
它会帮我们把 Dart 对象和数据库表之间的关系连起来,同时又不把 SQL 藏得太深。也就是说,你还是要会一点 SQL,但是不用自己手写一堆打开数据库、解析结果、塞回对象的东西。
也就是把原始的 SQL 操作封装了一遍
名字里多出来的那个 F 大概就是 Flutter 的 F 吧,挺 F 的
和 Room 完全一致的三件套
Section titled “和 Room 完全一致的三件套”Room 里最核心的是三个东西:
Entity:一张表Dao:操作这张表的方法Database:整个数据库入口
Froom 也是如此。
只不过 Froom 写的是 Dart。尽管语言不一样,但 Froom 心依然是 Room 心。
Entity
Section titled “Entity”Room 里一般会这样写:
@Entitydata class User( @PrimaryKey val id: Int, val name: String)Froom 里就变成这样:
import 'package:froom/froom.dart';
@entityclass User { @primaryKey final int id; final String name;
User(this.id, this.name);}可以看到差别其实不大,都是给类加个标记,然后告诉它哪个字段是主键。
只不过 Room 的注解是大写开头,Froom 这边是 Dart 风格的小写注解,看起来稍微有点不一样。
DAO 就是 Data Access Object(数据访问对象),说人话就是“负责查数据库的对象”。
Room 里一般这样写:
@Daointerface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User>
@Insert fun insert(user: User)}Froom 里就差不多这样:
@daoabstract class UserDao { @Query('SELECT * FROM User') Future<List<User>> findAllUsers();
@Query('SELECT * FROM User WHERE id = :id') Stream<User?> findUserById(int id);
@insert Future<void> insertUser(User user);}这里比较有 Flutter 味道的是返回值。
普通查询可以返回 Future,表示之后给你结果;如果想监听变化,可以返回 Stream,数据变了也还能继续收到。
这就和 Flutter 的异步对接上了,不然写本地数据库还要自己搞通知,也太麻烦了。
Database
Section titled “Database”Room 里要写一个继承 RoomDatabase 的抽象类:
@Database(entities = [User::class], version = 1)abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao}Froom 里也是抽象类,只不过继承的是 FroomDatabase:
import 'dart:async';import 'package:froom/froom.dart';import 'package:sqflite/sqflite.dart' as sqflite;
part 'database.g.dart';
@Database(version: 1, entities: [User])abstract class AppDatabase extends FroomDatabase { UserDao get userDao;}那个 part 'database.g.dart'; 一看就知道要动用代码生成了。
也就是说,这玩意和 Room 一样,不是写完就完事了,还要再生成一波(只不过 Room 可以在启动的时候自动生成)。
想用 Froom 的话要装三个东西:
dependencies: froom: ^6.0.0
dev_dependencies: froom_generator: ^6.0.0 build_runner: ^2.15.0写完对应代码后跑一下:
flutter packages pub run build_runner build然后就会生成对应的 database.g.dart。
真的是太简单了!除了生成是有点麻烦,Flutter 就这点不太好。
生成之后,这样就能拿到数据库:
final database = await $FroomAppDatabase .databaseBuilder('app_database.db') .build();
final userDao = database.userDao;
await userDao.insertUser(User(1, 'Yiyue'));final users = await userDao.findAllUsers();这个 $FroomAppDatabase 就是生成出来的类,名字会根据 AppDatabase 的名字变。
如果之前用过 Room,那这里就完全一致了:先来个数据库,再得到 DAO,然后通过 DAO 去增删查改。
如果要用数据库的话,那用 Froom 还是会比摁造 SQL 轻松些的
特别是本来就熟悉 Room 的话,Froom 上手会很快,因为它们的思路完全一致的:
先写表,再写 DAO,再写数据库,最后代码生成一键清除。
既然 Froom 是 Room 的精神续作,那肯定也少不了一堆注解。
不过这些东西看起来很多,实际上就是在告诉生成器:这个类是什么、这个字段怎么存、这个方法要干什么。
生成器:你不说清楚我怎么知道啊。
@entity或者@Entity(...)用来把一个类标记成数据库表。
如果只是普通表,用小写的@entity就够了:
@entityclass User { @primaryKey final int id; final String name;
User(this.id, this.name);}如果想改表名、加索引、外键、联合主键,那就要用大写的@Entity(...):
@Entity( tableName: 'users', primaryKeys: ['id', 'name'], indices: [Index(value: ['name'])],)class User { final int id; final String name;
User(this.id, this.name);}常用参数大概是这些:
tableName:自定义表名primaryKeys:联合主键indices:索引foreignKeys:外键withoutRowid:SQLite 的WITHOUT ROWID表,这个一般用不到
@primaryKey或者@PrimaryKey(...)用来标记主键。
如果主键想自动生成,就这样:
@PrimaryKey(autoGenerate: true)final int id;这点和 Room 也很像,只不过文档里说主键字段要是 int。
@ColumnInfo(...)用来改列名:
@ColumnInfo(name: 'user_name')final String name;如果 Dart 里的字段名和数据库里的列名不想一样,就靠它了。
@ignore用来忽略字段:
@ignoreString? tempText;getter、setter 和 static 字段默认就会被忽略,所以不是所有东西都要自己标。
ForeignKey(...)和Index(...)严格来说不是直接贴在字段上的注解,更像是塞进@Entity(...)里的配置。
外键大概这样:
@Entity( foreignKeys: [ ForeignKey( childColumns: ['owner_id'], parentColumns: ['id'], entity: User, ), ],)class Dog { @primaryKey final int id;
@ColumnInfo(name: 'owner_id') final int ownerId;
Dog(this.id, this.ownerId);}也就是说,childColumns是这张表自己的列,parentColumns是被引用表里的列。
索引则是这样:
@Entity( indices: [ Index(value: ['name'], unique: true), ],)class User { @primaryKey final int id; final String name;
User(this.id, this.name);}索引可以让查询快一点,但也不是越多越好,不然数据库写入的时候也要维护一大坨索引,最后就反过来拖后腿了。
DAO 相关
Section titled “DAO 相关”@dao用来标记 DAO 类。
@daoabstract class UserDao { // 这里放查询和增删改}@Query(...)用来写 SQL 查询:
@Query('SELECT * FROM User WHERE id = :id')Future<User?> findUserById(int id);这里的:id会和方法参数id对应起来。
如果查不到单个对象,返回值就要写成可空,比如Future<User?>,不然就有点自欺欺人了。
@insert和@Insert(...)用来插入:
@insertFuture<void> insertUser(User user);
@Insert(onConflict: OnConflictStrategy.replace)Future<int> insertAndReturnId(User user);小写的@insert就是默认插入,大写的@Insert(...)可以设置冲突策略。
返回值也不只能是void,还可以是插入后的主键,比如int或者List<int>。
@update和@Update(...)用来更新:
@updateFuture<void> updateUser(User user);
@Update(onConflict: OnConflictStrategy.replace)Future<int> updateAndCount(User user);如果返回int,就是改了多少行。
@delete用来删除:
@deleteFuture<int> deleteUser(User user);返回int的话就是删了多少行。
@transaction用来把几个操作包成一个事务:
@transactionFuture<void> replaceUsers(List<User> users) async { await deleteAllUsers(); await insertUsers(users);}这个方法需要写成async,返回值也要是Future。
事务就是一荣俱荣一损俱损的“要么全做完,要么都别做”,不然删完旧数据、插入新数据失败,那就很遗憾了。
@Database(...)用来标记数据库类:
@Database(version: 1, entities: [User])abstract class AppDatabase extends FroomDatabase { UserDao get userDao;}常用参数就是:
version:数据库版本entities:有哪些表views:有哪些数据库视图
如果之后表结构变了,就要改version,然后写迁移:
final migration1to2 = Migration(1, 2, (database) async { await database.execute('ALTER TABLE users ADD COLUMN nickname TEXT');});
final database = await $FroomAppDatabase .databaseBuilder('app_database.db') .addMigrations([migration1to2]) .build();迁移本身不是注解,但它经常和@Database(version: ...)一起出现,所以也放上来了。
视图和类型转换
Section titled “视图和类型转换”@DatabaseView(...)用来定义数据库视图。
视图可以理解成一个提前写好的SELECT,像虚拟表一样查询,但是不能插入、更新、删除。
@DatabaseView( 'SELECT distinct(name) AS name FROM users', viewName: 'user_names',)class UserName { final String name;
UserName(this.name);}定义完之后还要放进数据库:
@Database(version: 1, entities: [User], views: [UserName])abstract class AppDatabase extends FroomDatabase { UserDao get userDao;}如果只是想查一些拼出来的数据,视图会很方便。
TypeConverter和@TypeConverters(...)用来处理 SQLite 不认识的类型。
比如 SQLite 不知道 Dart 的DateTime是什么,那就把它变成int存进去:
class DateTimeConverter extends TypeConverter<DateTime, int> { @override DateTime decode(int databaseValue) { return DateTime.fromMillisecondsSinceEpoch(databaseValue); }
@override int encode(DateTime value) { return value.millisecondsSinceEpoch; }}
@TypeConverters([DateTimeConverter])@Database(version: 1, entities: [Order])abstract class OrderDatabase extends FroomDatabase { OrderDao get orderDao;}这个功能还是实验性的,所以能不用复杂类型就先别用吧。
@fts3、@fts4或者大写的@Fts3(...)、@Fts4(...)是全文搜索用的。
普通 CRUD 基本碰不到它们,如果之后要做搜索,尤其是对一大堆文本做搜索,才需要看这一块。
它们可以设置 tokenizer,比如:
@Fts4(tokenizer: FtsTokenizer.unicode61)@entityclass Article { final String title; final String content;
Article(this.title, this.content);}这玩意已经有点太贴近 SQLite 了,但是还是放上来,因为有这玩意
最后整理一下
Section titled “最后整理一下”一般应该也只能用到这些:
- 表:
@entity、@primaryKey、@ColumnInfo、@ignore - DAO:
@dao、@Query、@insert、@update、@delete、@transaction - 数据库:
@Database - 比较高手:
@Entity(...)、Index、ForeignKey、@DatabaseView、@TypeConverters - 特殊场景:
@fts3、@fts4
看起来很多,但实际写项目的时候,一开始可能只会用到前两行。
剩下的东西应该也用不到了吧