目录
入门
Drift 提供了一个dart_api来定义表和编写 SQL 查询。尤其当您已经熟悉 SQL 时,直接在 SQL 中使用CREATE TABLE语句定义表可能会更方便。得益于 Drift 内置的强大 SQL 解析器和分析器,您仍然可以运行类型安全的 SQL 查询,并支持自动更新流和所有其他 Drift 功能。SQL 的有效性在构建时进行检查,Drift 会为每个表和 SQL 语句生成匹配的方法。
设置
添加漂移依赖项的基本设置与 dart_apis 的设置一致。具体描述请参阅设置页面。
不同之处在于表和查询的声明方式。为了让Drift识别SQL,需要将其放入.drift文件中。在此示例中,我们.drift在数据库类旁边使用了一个名为的文件tables.drift:
dart
-- this is the tables.drift file
CREATE TABLE todos (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT,
body TEXT,
category INT REFERENCES categories (id)
);
CREATE TABLE categories (
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
description TEXT
) AS Category; -- see the explanation on "AS Category" below
/* after declaring your tables, you can put queries in here. Just
write the name of the query, a colon (:) and the SQL: */
todosInCategory: SELECT * FROM todos WHERE category = ?;
/* Here's a more complex query: It counts the amount of entries per
category, including those entries which aren't in any category at all. */
countEntries:
SELECT
c.description,
(SELECT COUNT(*) FROM todos WHERE category = c.id) AS amount
FROM categories c
UNION ALL
SELECT null, (SELECT COUNT(*) FROM todos WHERE category IS NULL);
Drift 会为您的表生成 Dart 类,这些类的名称基于表名。默认情况下,Drift 会去除s表尾的空格。这在大多数情况下都适用,但在某些情况下(例如categories上表)则不行。我们希望生成一个 Category类(而不是Categorie),所以我们告诉 Drift 生成一个不同的名称,并AS 在末尾添加声明。
将漂移文件集成到数据库很简单,只需将其添加到 注释include 的参数中即可**@DriftDatabase**。tables这里可以省略该参数,因为没有 Dart 定义的表需要添加到数据库中。
dart
import 'dart:io';
import 'package:drift/drift.dart';
// These imports are used to open the database
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'database.g.dart';
@DriftDatabase(
// relative import for the drift file. Drift also supports `package:`
// imports
include: {'tables.drift'},
)
class AppDb extends _$AppDb {
AppDb() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
// the LazyDatabase util lets us find the right location for the file async.
return LazyDatabase(() async {
// put the database file, called db.sqlite here, into the documents folder
// for your app.
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase.createInBackground(file);
});
}
要生成database.g.dart 包含**_$AppDb** 超类的文件,请dart run build_runner build在命令行上运行。
漂移文件
Drift 文件是一项新功能,允许您使用 SQL 编写所有数据库代码。但与您传递给简单数据库客户端的原始 SQL 字符串不同,Drift
文件中的所有内容都经过 Drift 强大的 SQL 分析器验证。这使您能够更安全地编写 SQL 查询:Drift
会在构建过程中发现其中的错误,并为其生成类型安全的 dart_api,这样您就无需手动读取结果。
入门
要使用此功能,我们需要创建两个文件:database.dart和tables.drift。 Dart 文件仅包含设置数据库的最少代码:
dart
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
@DriftDatabase(
include: {'tables.drift'},
)
class MyDb extends _$MyDb {
// This example creates a simple in-memory database (without actual
// persistence).
// To store data, see the database setups from other "Getting started" guides.
MyDb() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
}
我们现在可以在漂移文件中声明表和查询:
dart
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; -- the AS xyz after the table defines the data class name
-- You can also create an index or triggers with drift files
CREATE INDEX categories_description ON categories(description);
-- we can put named SQL queries in here as well:
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
deleteById: DELETE FROM todos WHERE id = :id;
allTodos: SELECT * FROM todos;
使用 运行构建运行器后dart run build_runner build,drift 将写入database.g.dart 包含_$MyDb超类的文件。让我们看看我们得到了什么:
- 生成的数据类(Todo和Category)以及用于插入的配套版本(更多信息请参阅Dart Interop)。默认情况下,drift
会从类的表名中去掉尾部的"s"。这就是为什么我们AS Category在第二个表上使用 ------ 否则它就会被 Categorie这样调用。 - 运行查询的方法:
- 一个Future createEntry(String title, String
content)方法。它使用提供的数据创建一个新的待办事项条目,并返回所创建条目的 ID。 - Future deleteById(int id):根据 ID 删除待办事项条目,并返回受影响的行数。
- Selectable
allTodos()。它可用于获取或查看所有待办事项。它可以与allTodos().get()和 一起
使用allTodos().watch()。 - 不匹配表的选择语句的类。在上面的例子中,该类AllTodosResult包含所有字段 todos以及相关类别的描述。
变量
在命名查询中,您可以像在 SQL 中一样使用变量。我们支持常规变量 ( ?)、显式索引变量 ( ?123) 和冒号命名变量 ( :id)。我们不支持使用 @ 或 $ 声明的变量。编译器将尝试通过查看变量的上下文来推断其类型。这使得 Drift 能够为您的查询生成类型安全的 API,变量将作为参数写入您的方法。
当变量类型不明确时,分析器可能无法解析该变量的类型。对于这些情况,你也可以指定变量的显式类型:
dart
myQuery(:variable AS TEXT): SELECT :variable;
除了基类型之外,还可以声明类型可为空:
dart
myNullableQuery(:variable AS TEXT OR NULL): SELECT :variable;
最后,你可以在 Dart 中使用命名参数时声明一个变量是必需的。为此,请添加一个REQUIRED关键字:
dart
myRequiredQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable;
named_parameters 请注意,这仅在启用构建选项时才有效。此外,默认情况下需要非空变量。
数组
如果要检查某个值是否在值数组中,可以使用IN ?。这不是有效的 SQL,但 Drift 会在运行时对其进行语法糖解析。因此,对于以下查询:
dart
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 = ?会按预期工作。
定义表
在.drift文件中,您可以使用CREATE TABLE语句定义表,就像在 SQL 中编写一样。
支持的列类型
就像 sqlite 本身一样,我们使用此算法 根据声明的类型名称来确定列类型。
此外,类型名为BOOLEAN或DATETIME的 列,其 Dart 对应类型为bool或DateTime。布尔值存储为INTEGER(0或1)。日期时间存储为 unix 时间戳(INTEGER)或 ISO-8601 时间戳(TEXT),具体取决于可配置的构建选项。对于在 Dart 中应表示为 的整数BigInt(即,为了在编译为 JS 时更好地兼容大数),请使用 类型定义列INT64。
ENUM()Dart 枚举可以通过使用引用 Dart 枚举类的类型自动按其索引进行存储:
dart
enum Status {
none,
running,
stopped,
paused
}
import 'status.dart';
CREATE TABLE tasks (
id INTEGER NOT NULL PRIMARY KEY,
status ENUM(Status)
);
有关存储枚举的更多信息,请参阅类型转换器页面。除了使用按索引映射枚举的整数之外,您还可以按名称存储它们。为此,请使用ENUMNAME(...)而不是ENUM(...)。
有关所有支持类型的详细信息,以及如何在日期时间模式之间切换的信息,请参阅本节。
表达式中还支持其他特定于漂移的类型(BOOLEAN、和) DATETIME, 这对于视图很有帮助:ENUMENUMNAMECAST
dart
CREATE VIEW with_next_status AS
SELECT id, CAST(status + 1 AS ENUM(Status)) AS status
FROM tasks
WHERE status < 3;
漂移特有的功能
为了支持 Drift 的 dart_api,CREATE TABLEDrift 文件中的语句可以使用 Dart 特有功能的特殊语法。当然,Drift 会CREATE TABLE 在运行语句之前删除这些特殊语法。
- 您可以通过附加到语句来为表或定义的查询定义自定义行类。WITH YourDartClassCREATE TABLE
- 或者,您可以使用AS DesiredRowClassName来更改由漂移生成的行类的名称。
- 自定义行类和自定义表名也适用于视图,例如 CREATE VIEW my_view AS DartName AS SELECT ...;。
- 在列定义中,MAPPED BY可用于将转换器应用于 该列。
- 类似地,JSON KEY可以使用约束来定义在将该表的一行序列化为 JSON 时将使用的键漂移。
- 最后,AS getterName可以用作列约束来覆盖 Dart 中该列的生成名称。当 SQL
中该列的名称所启发的默认列名与生成的表类的其他成员冲突时,此功能非常有用。
导入
您可以将导入语句放在文件顶部drift:
dart
import 'tables.drift'; -- single quotes are required for imports
所有可从其他文件访问的表也将在当前文件及其数据库中可见includes。如果您想对另一个漂移文件中定义的表声明查询,则还需要导入该文件以使这些表可见。请注意,漂移文件中的导入始终具有传递性,因此在上面的示例中,您也将拥有所有在可用文件中声明的导入。漂移文件other.drift没有机制。export
您也可以将 Dart 文件导入到漂移文件中,这样,所有通过 Dart 表声明的表都可以在查询中使用。我们支持相对导入和package:您熟悉的 Dart 导入。
嵌套结果
许多查询通常使用 SELECT table.*以下语法来获取某个表的所有列。当应用于来自连接的多个表时,这种方法可能会变得有点繁琐,如下例所示:
dart
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 saved_routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
为了匹配返回的列名,同时避免 Dart 中的名称冲突,drift 会生成一个包含、 、id、name和 一个字段的类。当然,这根本没用------这又是从 还是 来的? 让我们重写查询,这次使用嵌套结果:id1latlonglat1long1lat1fromto
dart
routesWithNestedPoints: SELECT r.id, r.name, f.** AS "from", t.** AS "to" FROM saved_routes r
INNER JOIN coordinates f ON f.id = r."from"
INNER JOIN coordinates t ON t.id = r."to";
如您所见,我们只需使用特定于漂移的 table.**语法即可嵌套结果。对于此查询,漂移将生成以下类:
dart
class RoutesWithNestedPointsResult {
final int id;
final String name;
final Point from;
final Point to;
// ...
}
太棒了!这个类比之前的平面结果类更符合我们的意图。
这些嵌套结果列 ( **) 只能出现在顶级 select 语句中,复合 select 语句或子查询尚不支持它们。但是,它们可以引用 SQL 中已连接到 select 语句的任何结果集,包括子查询和表值函数。
你可能想知道它的内部工作原理,因为它不是有效的 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。
LIST子查询
从 Drift 版本开始1.4.0,子查询也可以作为完整列表进行选择。只需将子查询放在LIST()函数中,即可将子查询的所有行包含在结果集中。
重新使用嵌套结果示例中介绍的coordinates 和表,我们添加一个存储沿路线坐标的新表:saved_routes
dart
CREATE TABLE route_points (
route INTEGER NOT NULL REFERENCES saved_routes (id),
point INTEGER NOT NULL REFERENCES coordinates (id),
index_on_route INTEGER,
PRIMARY KEY (route, point)
);
现在,假设我们想要查询一条包含沿途所有点信息的路线。虽然这需要两条 SQL 语句,但我们可以将其写成一条漂移查询,然后自动拆分成两条语句:
dart
routeWithPoints: SELECT
route.**,
LIST(SELECT coordinates.* FROM route_points
INNER JOIN coordinates ON id = point
WHERE route = route.id
ORDER BY index_on_route
) AS points
FROM saved_routes route;
这将生成一个结果集,其中包含一个SavedRoute route字段以及 List points沿途所有点的列表。
在内部,Drift 会将此查询拆分为两个单独的查询:- 外部SELECT route.** FROM saved_routes routeSQL 查询 -SELECT coordinates.* FROM route_points ... ORDER BY index_on_route为外部查询中的每一行运行一个单独的查询。route.id内部查询中的引用将被替换为一个变量,Drift 会将该变量绑定到外部查询中的实际值。
虽然LIST()子查询是一个非常强大的功能,但当外部查询有很多行时(因为内部查询针对每个外部行执行),它们的成本可能很高。
Dart 互操作
Drift 文件与 Drift 现有的 dart_api 完美协同工作:
- 您可以为漂移文件中声明的表编写 Dart 查询:
dart
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
- 通过将 Dart 文件导入到漂移文件中,您可以为 Dart 中声明的表编写 SQL 查询。
- 生成的查询方法可用于事务,它们与自动更新查询等一起工作。
如果您在生成的 Dart 类中使用fromJson和toJson方法,并且需要更改 json 中列的名称,则可以使用JSON KEY列约束来执行此操作,因此id INT NOT NULL JSON KEY userId 会在 json 中生成序列化为"userId"的列。
SQL 中的 Dart 组件
你可以使用"Dart 模板"来充分利用 SQL 和 Dart 语言的优势。Dart 模板是一种 Dart 表达式,可以在运行时内联到查询语句中。要使用它们,请在查询语句中声明一个 $ 变量:
filterTodos: SELECT * FROM todos WHERE $predicate;
Drift 将生成一个Selectable带有predicate参数的方法,可用于在运行时构建动态过滤器:
dart
Stream<List<Todo>> watchInCategory(int category) {
return filterTodos((todos) => todos.category.equals(category)).watch();
}
这让你可以编写单个 SQL 查询并在运行时动态应用谓词!此功能适用于
- 表达式,正如您在上面的示例中看到的那样
- 单一排序项:SELECT * FROM todos ORDER BY $term, id ASC
将生成一个采用的方法OrderingTerm。 - 整个 order-by 子句:SELECT * FROM todos ORDER BY $order
- 限制条款:SELECT * FROM todos LIMIT $limit
- 插入语句的插入项:INSERT INTO todos $row生成一个Insertable row 参数
当用作表达式时,您还可以在查询中提供默认值:
dart
getTodos ($predicate = TRUE): SELECT * FROM todos WHERE $predicate;
这将使该参数在 Dart 中成为可选参数。如果未明确设置,predicate它将使用默认的 SQL 值(此处为)。TRUE
类型转换器
您可以在漂移文件中导入并使用用 Dart 编写的类型转换器 。导入 Dart 文件需要使用常规import语句。要在列定义上应用类型转换器,可以使用MAPPED BY列约束:
dart
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT,
preferences TEXT MAPPED BY `const PreferenceConverter()`
);
引用带有类型转换器的表列的查询或视图也将继承该转换器。此外,查询和视图都可以为特定列指定类型转换器:
dart
CREATE VIEW my_view AS SELECT 'foo' MAPPED BY `const PreferenceConverter()`
SELECT
id,
json_extract(preferences, '$.settings') MAPPED BY `const PreferenceConverter()`
FROM users;
使用类型转换器时,我们推荐使用apply_converters_on_variables build 选项。这也会将转换器从 Dart 应用于 SQL,例如,如果用于变量:SELECT * FROM users WHERE preferences = ?。使用该选项,变量将被推断为 ,Preferences而不是String。
Drift 文件还对隐式枚举转换器有特殊支持:
dart
import 'status.dart';
CREATE TABLE tasks (
id INTEGER NOT NULL PRIMARY KEY,
status ENUM(Status)
);
当然,关于自动枚举转换器的警告也适用于漂移文件。
现有的行类
您可以使用自定义行类,而不必让 Drift 为您生成一个。例如,假设您有一个 Dart 类定义为
dart
class User {
final int id;
final String name;
User(this.id, this.name);
}
然后,您可以指示漂移将该类用作行类,如下所示:
dart
import 'row_class.dart'; --import for where the row class is defined
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL
) WITH User; -- This tells drift to use the existing Dart class
当使用在其他 Dart 文件中定义的自定义行类时,您还需要将该文件导入到定义数据库的文件中。有关此功能的更多常规信息,请查看此页面。
自定义行类可应用于文件SELECT中定义的查询.drift。要使用自定义行类,WITH可在查询名称后添加语法。
例如,假设我们row_class.dart通过添加另一个类来扩展现有的 Dart 代码:
dart
class UserWithFriends {
final User user;
final List<User> friends;
UserWithFriends(this.user, {this.friends = const []});
}
现在,我们可以使用新类为其行添加相应的查询:
dart
-- table to demonstrate a more complex select query below.
-- also, remember to add the import for `UserWithFriends` to your drift file.
CREATE TABLE friends (
user_a INTEGER NOT NULL REFERENCES users(id),
user_b INTEGER NOT NULL REFERENCES users(id),
PRIMARY KEY (user_a, user_b)
);
allFriendsOf WITH UserWithFriends: SELECT users.** AS user, LIST(
SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id OR user_a = users.id
) AS friends FROM users WHERE id = :id;
该WITH UserWithFriends语法将使 Drift 考虑UserWithFriends类。对于构造函数中的每个字段,Drift 都会检查查询中的列,并验证其是否具有兼容类型。然后,Drift 会在内部生成查询代码,将行映射到该类的实例 UserWithFriends。
有关使用自定义行类进行查询的更完整概述,请参阅 查询部分。
Dart 文档注释
漂移文件中列前添加的注释将作为 Dart 文档注释添加到生成的行类中:
dart
CREATE TABLE friends (
-- The user original sending the friendship request
user_a INTEGER NOT NULL REFERENCES users(id),
-- The user accepting the friendship request from [userA].
user_b INTEGER NOT NULL REFERENCES users(id),
PRIMARY KEY (user_a, user_b)
);
漂移生成的类中生成的userA和字段将会有这些注释作为文档注释。userBFriend
结果类名称
对于大多数查询,漂移会生成一个新的类来保存结果。该类以查询名称加上后缀命名Result,例如,一个myQuery查询会得到一个MyQueryResult类。
您可以像这样更改结果类的名称:
dart
routesWithNestedPoints AS FullRoute: SELECT r.id, -- ...
这样,多个查询也可以共享一个结果类。只要它们具有相同的结果集,您就可以为它们分配相同的自定义名称,并且漂移只会生成一个类。
对于仅选择表中所有列的查询,drift 不会生成新的类,而是会重用它生成的数据类。同样,对于只有一列的查询,drift 会直接返回该列,而不是将其包装在结果类中。目前无法覆盖此行为,因此,如果查询包含匹配的表或只有一列,则您无法自定义该查询的结果类名称。
支持的语句
目前,.drift文件中可以出现以下语句。
- import 'other.drift':将另一个文件中声明的所有表和查询导入到当前文件中。
- DDL 语句:您可以将CREATE TABLE、、CREATE VIEW和CREATE INDEX语句CREATE
TRIGGER放入漂移文件中。 - 查询语句:我们支持INSERT、、SELECT和UPDATE语句DELETE。
所有导入都必须位于 DDL 语句之前,并且这些语句必须位于命名查询之前。
如果您需要另一个语句的支持,或者如果漂移拒绝您认为有效的查询,请创建一个问题!