「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。
本文翻译自 drift 的 官方文档 Drift files (simonbinder.eu)。
肉翻多有不足,不吝赐教。
drift文件
学习有关 .drift 文件的一切。drift 文件可包含表和查询。
drift 文件是一个新特性,可以在 sql 中写所有的数据库代码 - drift 会为为它们生成类型安全的 api 。
开始
要使用这个特性,需要创建两个文件: database.dart 和 tables.drift。 Dart 文件只包含准备数据库的最少代码。
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
@DriftDatabase(
include: {'tables.drift'},
)
class MyDb extends _$MyDb {
MyDb() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
}
现在可以在 drift 文件中声明表和查询。
CREATE TABLE todos (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category INTEGER REFERENCES categories(id)
);
CREATE TABLE categories (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL
) AS Category; -- 表后面的 AS xyz 定义了数据类的类名。
-- 也可以用 drift 文件创建索引或触发器
CREATE INDEX categories_description ON categories(description);
-- 也可以把命名的 sql 查询放在这里:
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
deleteById: DELETE FROM todos WHERE id = :id;
allTodos: SELECT * FROM todos;
使用 flutter pub run build_runner build 运行构建之后, drift 会写 database.g.dart 文件,该文件中包含 _$MydDb 父类。看一下生成了什么:
-
生成的数据类(
Todo和Category),用于插入的数据类的姊妹版(相关信息看下 Dart Interop )。默认情况下,drift 会去掉表名末尾的s作为类名。这是为什么第二个表使用了AS Category,否则类名会变成Categorie。 -
用于运行查询的方法:
- 一个
Future createEntry(String title, String content)方法。用提供的数据创建一个新的 todo 实体,然后返回创建的实体的 id 。 Future deleteById(int id):用 id 删除一个 todo 实体,返回受影响的行数。Selectable allTodos()。可以用来获取或监视所有的 todo 实体。可以通过allTodos().get()和allTodos().watch()来使用。
- 一个
-
用于不匹配某一个表的 select 语句的类。在上面的例子中是
AllTodosResult类,包含todos中的所有字段和关联的 category 的描述。
变量
如果想要检查一个值是否在一个值数组中,可以使用 IN ?。这不是正确的 sql ,但是 drift 会在运行时提取(为正确的 sql )。所以对于以下查询:
entriesWithId: SELECT * FROM todos WHERE id IN ?;
drift 会生成一个 Selectable entriesWithId(List ids) 方法。运行 entriesWithId([1,2]) 会生成 SELECT * ... id IN (?1, ?2),然后相应地绑定参数。要确保这会如期望运行, drift 利用了两个小的限制:
- 非指定的变量:
WHERE id IN ?2在构建时会被拒绝。因为变量是展开的,为其设置一个单值是无效的。 - 不能多于变量后指定的下标:运行
WHERE id IN ? OR title = ?2也会被拒绝。展开的变量会和指定的下标冲突,这也是 drift 会禁止的原因。当然id IN ? OR title = ?会如期望运转。
支持的列类型
使用这种运算法则 基于声明的类型名来明确列的类型。
此外,类型名为 BOOLEAN 或 DATETIME 列在 Dart 中对应的是 bool 或 Datetime 类型。当表创建时会作为 INTEGER 列写入。
import(导入)
可以把 import 语句做为 drift 文件的第一句。
import 'other.drift'; -- 需要用单引号写 import
从其它文件中能获取的表会之后也在当前文件也会可见,对于 inscludes 它的数据库也会可见。如果想要声明对其它 drift 文件中定义的表的查询,只需要导入对于这个表可见的文件。注意 drift 文件中的导入总是可传递的,所以在上面的例子中也会拥有所有的在 other.drift 中可见的 import 。 drift 文件没有 export (导出)机制。
在 drift 文件中导入 Dart 文件也能正常运转 - 导入之后,所有通过 Dart 表声明的表可以用作内部查询。该机制同时支持相对导入和已知的来自 Dart 的 package: 导入。
嵌套结果
很多查询从一些表里获取所有列,具有代表性的是使用: SELECT table.* 语法。当对多个表进行结合时,这个方式会比较晦涩,如下所示:
CREATE TABLE coordinates (
id INTEGER NOT NULL PRIMARY KEY,
lat REAL NOT NULL,
long REAL NOT NULL
);
CREATE TABLE saved_routes (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
"from" INTEGER NOT NULL REFERENCES coordinates (id),
to INTEGER NOT NULL REFERENCES coordinates (id)
);
routesWithPoints: SELECT r.id, r.name, f.*, t.* FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r.to;
要在 Dart 中避免名称冲突时匹配返回列的名称,drift 会生成一个有 id 、 name 、 id1 、 lat 、 long 、 lat1 和 long1 字段的类。当然,这一点儿也没用 - lat1 是来自于 from ,还是来自于 to ?来重写一下查询,这次使用嵌套结果:
routesWithNestedPoints: SELECT r.id, r.name, f.**, t.** FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r.to;
正如看到的,可以简单地使用 drift 指定的 table.** 语法来嵌套一个结果。对于这个查询, drift 会生成如下的类:
routesWithNestedPoints: SELECT r.id, r.name, f.**, t.** FROM routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r.to;
太棒了!这个类比之前的平铺的结果更符合预期。
现在这个时点,这个文件会有一些限制:
**还不支持复合查询- 如果 table 是一个实表或对于实表的引用,只能使用
table.**,尤其是,对于WITH子句或表-值函数的结果集,这个方式不能正常使用。
你可能想知道 ** 在底层是如何工作的,因为它不是正确的 sql 。在构建时, drift 生成器会将 ** 转换为一个引用表中所有列的列表。例如:如果有一个表 foo,它有一个 id INT 和一个 bar TEXT 列。之后,SELECT foo.** FROM foo 会被提取为 SELECT foo.id AS "nested_0.id", foo.bar AS "nested_0".bar FROM foo。
Dart 的交互
drift 文件可以完美和和 dart 的 Dart api 完美共用。
- 可以在 drift 文件里写对表的 Dart 查询。
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
- 通过在 drift 文件中导入 Dart 文件,可以对在 Dart 中声明的表写 sql 查询。
- 生成查询方法,这些方法可用于事务。这些方法可以和自动更新的查询共用,等等。
如果在生成的 Dart 类中使用 fromJson 和 toJson 方法,然后需要在 json 中改变列名,可以使用 JSON KEY 的列约束来做到这一点,所以 id INT NOT NULL JSON KEY userId 生成的列在 json 中会序列化为 "userId"。
sql 中的 Dart 组件
可以用 “Dart Templates” 生成大多数的 sql 和 Dart,“Dart Templates” 在运行时会将 Dart 表达式内联到一个查询上。要使用它们,需要在查询中声明 $-variable 。
_filterTodos: SELECT * FROM todos WHERE $predicate;
Dart 会生成一个 Selectable _filterTodos(Expression predicate) 方法,可用来在运行时构造动态的过滤器。
Stream<List<Todo>> watchInCategory(int category) {
return _filterTodos(todos.category.equals(category)).watch();
}
这允许编写 sql 查询,然后在运行时动态应用断言。这个特性可用于:
- 表达式 ,正如上例。
- 单个排序术语(ordering terms):
SELECT * FROM todos ORDER BY $term, id ASC会生成带参数OrderingTerm的方法。 - 整个排序子句:
SELECT * FROM todos ORDER BY $order。 - limit 子句:
SELECT * FROM todos LIMIT $limit。 - 用于插入语句的
Insertable:INSERT INTO todos $row会生成一个Insertable row参数。
作为表达式使用时,也可以在查询中使用默认值:
_filterTodos ($predicate = TRUE): SELECT * FROM todos WHERE $predicate;
这会使用在 Dart 中 prediate 变为可选的。如果没有被明确设值,它会使用默认的 sql 值(这里是 TRUE )。
Dart 中使用的列名
如果使用表的别名,需要在 sql 查询时嵌入 Dart 表达式时指定别名。 考虑下下面的示例:
findRoutes: SELECT r.* FROM routes r INNER JOIN points "start" ON "start".id = r."start" INNER JOIN points "end" ON "end".id = r."end" WHERE $predicate如果想要在 Dart 中过滤
start坐标点,需要使用明确的别名:Future<List<Route>> routesByStart(int startPointId) { final start = alias(points, 'start'); return findRoutes(start.id.equals(startPointId)); }可以使
scoped_dart_components的构建选项可用,然后让生成器帮助你。如果这个选项可用, drift 会生成一个Expression Function(Routes r, Points start, Points end)作为参数,会使这个处理容易很多:Future<List<Route>> routesByStart(int startPointId) { return findRoutes((r, start, end) => start.id.equals(startPointId)); }
类型转换器
可以在 drift 文件中导入和使用类型转换器。用常规的 import 语句导入 dart 文件是可以的。要在表定义时使用类型转换器,可以使用 MAPPED BY 列约束:
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT,
preferences TEXT MAPPED BY `const PreferenceConverter()`
);
在 drift 文件中应用类型转换器的更多详细信息参考这里。
当使用类型转换器时,建议 apply_converters_on_variables 构建选项。这会从 Dart 向 sql 转换时应用转换器,例如对变量使用:SELECT * FROM users WHERE preferences = ?。用这个选项,变量会被推断为 Preferences 而不是 String 。
存在的数据行类
可以使用自定义的数据行类来代替 drift 生成的。例如,比方说有一个 Dart 类定义为:
class User {
final int id;
final String name;
User(this.id, this.name);
}
然后,可以如下用指示 drift 使用这个类作为一个数据行类:
import 'row_class.dart'; -- 导入数据行类定义所在的文件
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
) WITH User; 这里告诉 drift 使用既存的 Dart 类
在另外一个 Dart 文件中使用自定义数据行类时,也需要把这个文件导入定义数据库类的文件中。关于这个特性的更多信息,看一下这个页面。
结果类的类名
对于大多数据查询, drift 会生成一个新的类来保有结果。这个类的命名会在查询后带一个 Result 后缀。 例如: myQuery 查询会获取一个 MyQueryResult 类。
可以如下改变一个结果类的名称:
routesWithNestedPoints AS FullRoute: SELECT r.id, -- ...
用这种方式,多个查询也会共享单个结果类。正如他们有共同的结果集。可以给它们指定相同的自定义名称,然后 drift 会只生成一个类。
对于从同一个表里抽取所有列而没有其它处理的查询来说, drift 不会生成一个新的类,取而代之的是重新利用它生成(不管何种生成方式)的类。类似的,对于只有一个列的查询, drift 会直接返回这个列,而不是包装成一个结果类。现在的时点,还不能覆写这个行为,所以如果有匹配的表或只有一列,还不能自定义结果类的类名。
支持的语句
现在的时点, .drift 文件中可以重现以下语句。
import 'other.drift':将所有的声明表和查询的其它文件导入到当前文件中。- DDL 语句:可以把
CREATE TABLE、CREATE VIEW、CREATE INDEX和CREATE TRIGGER语句放入 drift 文件中。 - 查询语句:支持
INSERT、SELECT、UPDATE和DELETE语句。
所有的导入必须在 DDL 语句之前,DDL 语句必须在命名的查询之前。
如果需要对其它语句的支持,或者 drift 拒绝了一个正确的查询,请创建一个 issue 。