[官网文档翻译]Flutter持久化库drift - 使用sql - drift文件

11,937 阅读9分钟

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

本文翻译自 drift 的 官方文档 Drift files (simonbinder.eu)

肉翻多有不足,不吝赐教。


重要通知: moor 已改名为 drift 。更多信息[中文]。

drift文件

学习有关 .drift 文件的一切。drift 文件可包含表和查询。

drift 文件是一个新特性,可以在 sql 中写所有的数据库代码 - drift 会为为它们生成类型安全的 api 。

开始

要使用这个特性,需要创建两个文件: database.darttables.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 父类。看一下生成了什么:

  • 生成的数据类( TodoCategory ),用于插入的数据类的姊妹版(相关信息看下 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 利用了两个小的限制:

  1. 非指定的变量: WHERE id IN ?2 在构建时会被拒绝。因为变量是展开的,为其设置一个单值是无效的。
  2. 不能多于变量后指定的下标:运行 WHERE id IN ? OR title = ?2 也会被拒绝。展开的变量会和指定的下标冲突,这也是 drift 会禁止的原因。当然 id IN ? OR title = ? 会如期望运转。

支持的列类型

使用这种运算法则 基于声明的类型名来明确列的类型。

此外,类型名为 BOOLEANDATETIME 列在 Dart 中对应的是 boolDatetime 类型。当表创建时会作为 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 类中使用 fromJsontoJson 方法,然后需要在 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
  • 用于插入语句的 InsertableINSERT 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 。