Drift 提供了一个dart_api来定义表和编写 SQL 查询。尤其当您已经熟悉 SQL 时,直接在 SQL 中使用CREATE TABLE语句定义表可能会更方便。得益于 Drift 内置的强大 SQL 解析器和分析器,您仍然可以运行类型安全的 SQL 查询,并支持自动更新流和所有其他 Drift 功能。SQL 的有效性在构建时进行检查,Drift 会为每个表和 SQL 语句生成匹配的方法。
添加漂移依赖项的基本设置与 dart_apis 的设置一致。具体描述请参阅设置页面。
不同之处在于表和查询的声明方式。为了让Drift识别SQL,需要将其放入.drift文件中。在此示例中,我们.drift在数据库类旁边使用了一个名为的文件tables.drift:
-- 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 定义的表需要添加到数据库中。
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';
(
// relative import for the drift file. Drift also supports `package:`
// imports
include: {'tables.drift'},
)
class AppDb extends _$AppDb {
AppDb() : super(_openConnection());
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 文件仅包含设置数据库的最少代码:
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
(
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());
int get schemaVersion => 1;
}
我们现在可以在漂移文件中声明表和查询:
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超类的文件。让我们看看我们得到了什么:
在命名查询中,您可以像在 SQL 中一样使用变量。我们支持常规变量 ( ?)、显式索引变量 ( ?123) 和冒号命名变量 ( :id)。我们不支持使用 @ 或 $ 声明的变量。编译器将尝试通过查看变量的上下文来推断其类型。这使得 Drift 能够为您的查询生成类型安全的 API,变量将作为参数写入您的方法。
当变量类型不明确时,分析器可能无法解析该变量的类型。对于这些情况,你也可以指定变量的显式类型:
myQuery(:variable AS TEXT): SELECT :variable;
除了基类型之外,还可以声明类型可为空:
myNullableQuery(:variable AS TEXT OR NULL): SELECT :variable;
最后,你可以在 Dart 中使用命名参数时声明一个变量是必需的。为此,请添加一个REQUIRED关键字:
myRequiredQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable;
named_parameters 请注意,这仅在启用构建选项时才有效。此外,默认情况下需要非空变量。
如果要检查某个值是否在值数组中,可以使用IN ?。这不是有效的 SQL,但 Drift 会在运行时对其进行语法糖解析。因此,对于以下查询:
entriesWithId: SELECT * FROM todos WHERE id IN ?;
Drift 会生成一个Selectable entriesWithId(List ids)方法。运行后entriesWithId([1,2])会生成SELECT * … id IN (?1, ?2)并绑定相应的参数。为了确保其按预期工作,Drift 施加了两个小限制:
在.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 枚举类的类型自动按其索引进行存储:
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
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 在运行语句之前删除这些特殊语法。
您可以将导入语句放在文件顶部drift:
import 'tables.drift'; -- single quotes are required for imports
所有可从其他文件访问的表也将在当前文件及其数据库中可见includes。如果您想对另一个漂移文件中定义的表声明查询,则还需要导入该文件以使这些表可见。请注意,漂移文件中的导入始终具有传递性,因此在上面的示例中,您也将拥有所有在可用文件中声明的导入。漂移文件other.drift没有机制。export
您也可以将 Dart 文件导入到漂移文件中,这样,所有通过 Dart 表声明的表都可以在查询中使用。我们支持相对导入和package:您熟悉的 Dart 导入。
许多查询通常使用 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 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
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.**语法即可嵌套结果。对于此查询,漂移将生成以下类:
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。
从 Drift 版本开始1.4.0,子查询也可以作为完整列表进行选择。只需将子查询放在LIST()函数中,即可将子查询的所有行包含在结果集中。
重新使用嵌套结果示例中介绍的coordinates和表,我们添加一个存储沿路线坐标的新表:saved_routes
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 语句,但我们可以将其写成一条漂移查询,然后自动拆分成两条语句:
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()子查询是一个非常强大的功能,但当外部查询有很多行时(因为内部查询针对每个外部行执行),它们的成本可能很高。
Drift 文件与 Drift 现有的 dart_api 完美协同工作:
Future<void> insert(TodosCompanion companion) async {
await into(todos).insert(companion);
}
如果您在生成的 Dart 类中使用fromJson和toJson方法,并且需要更改 json 中列的名称,则可以使用JSON KEY列约束来执行此操作,因此id INT NOT NULL JSON KEY userId 会在 json 中生成序列化为“userId”的列。
你可以使用“Dart 模板”来充分利用 SQL 和 Dart 语言的优势。Dart 模板是一种 Dart 表达式,可以在运行时内联到查询语句中。要使用它们,请在查询语句中声明一个 $ 变量:
filterTodos: SELECT * FROM todos WHERE $predicate;
Drift 将生成一个Selectable带有predicate参数的方法,可用于在运行时构建动态过滤器:
Stream<List<Todo>> watchInCategory(int category) {
return filterTodos((todos) => todos.category.equals(category)).watch();
}
这让你可以编写单个 SQL 查询并在运行时动态应用谓词!此功能适用于
当用作表达式时,您还可以在查询中提供默认值:
getTodos ($predicate = TRUE): SELECT * FROM todos WHERE $predicate;
这将使该参数在 Dart 中成为可选参数。如果未明确设置,predicate它将使用默认的 SQL 值(此处为)。TRUE
您可以在漂移文件中导入并使用用 Dart 编写的类型转换器 。导入 Dart 文件需要使用常规import语句。要在列定义上应用类型转换器,可以使用MAPPED BY列约束:
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT,
preferences TEXT MAPPED BY `const PreferenceConverter()`
);
引用带有类型转换器的表列的查询或视图也将继承该转换器。此外,查询和视图都可以为特定列指定类型转换器:
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 文件还对隐式枚举转换器有特殊支持:
import 'status.dart';
CREATE TABLE tasks (
id INTEGER NOT NULL PRIMARY KEY,
status ENUM(Status)
);
当然,关于自动枚举转换器的警告也适用于漂移文件。
您可以使用自定义行类,而不必让 Drift 为您生成一个。例如,假设您有一个 Dart 类定义为
class User {
final int id;
final String name;
User(this.id, this.name);
}
然后,您可以指示漂移将该类用作行类,如下所示:
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 代码:
class UserWithFriends {
final User user;
final List<User> friends;
UserWithFriends(this.user, {this.friends = const []});
}
现在,我们可以使用新类为其行添加相应的查询:
-- 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 文档注释添加到生成的行类中:
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类。
您可以像这样更改结果类的名称:
routesWithNestedPoints AS FullRoute: SELECT r.id, -- ...
这样,多个查询也可以共享一个结果类。只要它们具有相同的结果集,您就可以为它们分配相同的自定义名称,并且漂移只会生成一个类。
对于仅选择表中所有列的查询,drift 不会生成新的类,而是会重用它生成的数据类。同样,对于只有一列的查询,drift 会直接返回该列,而不是将其包装在结果类中。目前无法覆盖此行为,因此,如果查询包含匹配的表或只有一列,则您无法自定义该查询的结果类名称。
目前,.drift文件中可以出现以下语句。
所有导入都必须位于 DDL 语句之前,并且这些语句必须位于命名查询之前。
如果您需要另一个语句的支持,或者如果漂移拒绝您认为有效的查询,请创建一个问题!
Drift 的核心库主要以 SQLite3 为目标平台编写。这体现在Drift 开箱即用的SQL 类型上——这些类型由 SQLite3 支持,并新增了一些由 Dart 处理的类型。
其他 Drift 支持有限的数据库通常支持更多类型。例如,Postgres 为持续时间、JSON 值、UUID 等提供专用类型。对于 sqlite3 数据库,您需要使用类型转换器 将这些值存储为 sqlite3 支持的类型。虽然类型转换器在这里也可以使用,但它们会指示 Drift 在后台使用常规文本列。例如,当数据库内置对 UUID 的支持时,这可能会导致语句效率降低,或与其他与同一数据库通信的应用程序出现问题。因此,Drift 允许使用“自定义类型”——未在核心drift包中定义且并非适用于所有数据库的类型。
当将漂移支持扩展到具有尚未被漂移覆盖的自身类型的新数据库引擎时,自定义类型是一个很好的工具。
除非您要扩展 Drift 以使用新的数据库包(这很棒,请联系我们!),否则您可能不需要自己实现自定义类型。像 Drift
这样的包drift_postgres已经为您定义了相关的自定义类型。
举个例子,假设我们有一个数据库,它原生支持Duration 通过interval类型传递值。我们使用的数据库驱动程序也原生支持Duration值,这意味着值可以通过预处理语句传递给数据库,也可以从行中读取,而无需手动转换。
Duration在这种情况下,将添加一个自定义类型类来实现对漂移的支持:
import 'package:drift/drift.dart';
class DurationType implements CustomSqlType<Duration> {
const DurationType();
String mapToSqlLiteral(Duration dartValue) {
return "interval '${dartValue.inMicroseconds} microseconds'";
}
Object mapToSqlParameter(Duration dartValue) => dartValue;
Duration read(Object fromSql) => fromSql as Duration;
String sqlTypeName(GenerationContext context) => 'interval';
}
此类型定义以下内容:
要在 Dart 表上定义自定义类型,请使用customType具有以下类型的列构建器方法:
import 'package:drift/drift.dart';
import 'type.dart';
class PeriodicReminders extends Table {
IntColumn get id => integer().autoIncrement()();
Column<Duration> get frequency => customType(const DurationType())
.clientDefault(() => Duration(minutes: 15))();
TextColumn get reminder => text()();
}
如示例所示,其他列约束clientDefault仍然可以添加到自定义列中。如果需要,您甚至可以组合自定义列和类型转换器。
这足以使大多数查询正常工作,但在某些高级场景中,您可能需要提供更多信息才能使用自定义类型。例如,当手动构造一个Variable或一个Constant带有自定义类型的 a 时,必须将自定义类型作为第二个参数添加到构造函数中。这是因为与内置类型不同,drift 没有一个中央寄存器来描述如何处理自定义类型值。
在 SQL 中,Drift 的内联 Dart语法可用于定义自定义类型:
import 'type.dart';
CREATE TABLE periodic_reminders (
id INTEGER NOT NULL PRIMARY KEY,
frequency `const DurationType()` NOT NULL,
reminder TEXT NOT NULL
);
请注意,漂移文件中对自定义类型的支持目前有限。例如,CAST表达式中目前不支持自定义类型。如果您对自定义类型的高级分析支持感兴趣,请提交问题或参与讨论,并描述您的用例,谢谢!
当为某些数据库管理系统仅支持的 SQL 类型定义自定义类型时,您的数据库将仅适用于这些数据库系统。例如,任何使用DurationType 上述定义的表都无法与 sqlite3 兼容,因为它使用的interval类型被 sqlite3 解释为整数——而interval xyz microsecondssqlite3 根本不支持该语法。
从 Drift 2.15 开始,可以根据所使用的方言定义不同行为的自定义类型。这可以用来为其他数据库系统构建 polyfill。首先,考虑一个将持续时间存储为整数的自定义类型,类似于类型转换器可能执行的操作:
class _FallbackDurationType implements CustomSqlType<Duration> {
const _FallbackDurationType();
String mapToSqlLiteral(Duration dartValue) {
return dartValue.inMicroseconds.toString();
}
Object mapToSqlParameter(Duration dartValue) {
return dartValue.inMicroseconds;
}
Duration read(Object fromSql) {
return Duration(microseconds: fromSql as int);
}
String sqlTypeName(GenerationContext context) {
return 'integer';
}
}
const durationType = DialectAwareSqlType<Duration>.via(
fallback: _FallbackDurationType(),
overrides: {
SqlDialect.postgres: DurationType(),
},
);
通过使用DialectAwareSqlType,您可以在 PostgreSQL 数据库上自动使用该interval类型,同时在 sqlite3 和其他数据库上回退到整数类型:
Column<Duration> get frequency => customType(durationType)
.clientDefault(() => Duration(minutes: 15))();
在分析.drift文件时,生成器会考虑可能存在的 sqlite3 扩展。但是,生成器无法识别数据库正在使用的 sqlite3 库,因此它会默认使用未启用任何扩展的旧版 sqlite3 库,并做出悲观的假设。使用类似 的包时,您将获得启用了 json1 和 fts5 扩展的最新 sqlite3 版本。您可以使用构建选项sqlite3_flutter_libs将此信息告知生成器。
要在漂移文件和编译查询中启用 json1 扩展,请修改 构建选项以包含 json1在该sqlite_module部分中。
SQLite 扩展程序不需要任何特殊表,并且适用于所有文本列。在漂移文件和编译查询中,json启用该扩展程序后,所有函数均可用。
由于 json 扩展是可选的,因此在 Dart 中启用它需要特殊的导入。 package:drift/extensions/json1.dart下面是一个在 Dart 中使用 json 函数的示例:
import 'package:drift/drift.dart';
import 'package:drift/extensions/json1.dart';
class Contacts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get data => text()();
}
(tables: [Contacts])
class Database extends _$Database {
// constructor and schemaVersion omitted for brevity
Future<List<Contacts>> findContactsWithNumber(String number) {
return (select(contacts)
..where((row) {
// assume the phone number is stored in a json key in the `data` column
final phoneNumber = row.data.jsonExtract<String, StringType>('phone_number');
return phoneNumber.equals(number);
})
).get();
}
}
您可以在sqlite.org上了解有关 json1 扩展的更多信息。
fts5 扩展提供了 SQLite 表中的全文搜索功能。要在漂移文件和编译查询中启用 fts5 扩展,请修改 构建选项以包含 fts5在该sqlite_module部分中。
就像您在使用 sqlite 时所期望的那样,您可以使用CREATE VIRTUAL TABLE语句在漂移文件中创建 fts5 表。
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
fts5 表上的查询按预期工作:
emailsWithFts5: SELECT * FROM email WHERE email MATCH 'fts5' ORDER BY rank;
fts5 中的bm25、highlight和snippet函数也可用于自定义查询。
在 Dart 中无法声明 fts5 表或在 fts5 表上进行查询。您可以在sqlite.org上了解有关 fts5 扩展的更多信息。
Geopoly 模块是R-Tree扩展的替代接口,它使用GeoJSON表示法 ( RFC-7946 ) 来描述二维多边形。Geopoly 包含以下函数:检测一个多边形是否包含于另一个多边形内或与另一个多边形重叠;计算多边形的封闭面积;对多边形进行线性变换;将多边形渲染为SVG 格式;以及其他类似的操作。
要geopoly在漂移文件和编译查询中启用扩展,请修改 构建选项以包含 geopoly在该sqlite_module部分中。
使用此扩展创建虚拟表的示例:
create virtual table geo using geopoly(geoID, a, b);
SQLite 会接受附加列(如上例中的geoID、a、 )中的任何类型,因此会为这些列生成一个类型,这并不总是很方便。为了避免这种情况,您可以像以下示例一样添加类型: b drift DriftAny
create virtual table geo using geopoly (
geoID INTEGER not null,
a INTEGER,
b
);
这将为列类型添加提示,然后 Dart 代码将更方便使用
您可以在sqlite.org上了解有关 geopoly 扩展的更多信息。
虽然 Drift 包含一个流畅的 API,可用于对大多数语句进行建模,但WITH子句或某些子查询等高级功能尚不支持。不过,您可以使用customSelect和等方法customStatement,通过手动编写 SQL 在数据库上运行高级语句。
对于大多数自定义查询,Drift 可以在编译时分析其 SQL,确保其有效性,并为其生成类型安全的 API。这种方法比在运行时编写自定义 SQL 安全得多。
本页描述了这两种方法:第一部分介绍了漂移生成的方法,第二部分给出了运行时定义的自定义查询的示例。
您可以指示 Drift 自动为您的 select、update 和 delete 语句生成类型安全的 API。当然,您仍然可以手动编写自定义 SQL。有关详情,请参阅以下部分。
要使用此功能,您只需在DriftDatabase注释中定义查询:
(
tables: [TodoItems, Categories],
queries: {
'categoriesWithCount': 'SELECT *, '
'(SELECT COUNT(*) FROM todo_items WHERE category = c.id) AS "amount" '
'FROM categories c;'
},
)
class MyDatabase extends $MyDatabase {
// rest of class stays the same
}
再次运行构建步骤后,drift 将为CategoriesWithCountResult您编写类 - 它将保存查询结果。此外,_$MyDatabase您继承的类将具有一个 Selectable categoriesWithCount()可用于运行查询的方法。与Selectable Drift 中的所有方法一样,您可以使用它get()来运行一次查询或watch()获取自动更新的结果流:
更好地支持漂移文件中的自定义查询
在注释中定义 SQL@DriftDatabase是定义一些自定义查询的好方法。对于使用大量自定义查询的应用,将它们提取到单独的文件中可能更易于管理。Drift 文件可以包含在数据库中,非常适合这种情况,并且可能更易于使用。
Future<void> useGeneratedQuery() async {
// The generated query can be run once as a future:
await categoriesWithCount().get();
// Or multiple times as a stream
await for (final snapshot in categoriesWithCount().watch()) {
print('Found ${snapshot.length} category results');
}
}
?查询可以使用或语法包含参数:name。对于查询中的参数,Drift 会确定合适的类型并将其包含在生成的方法中。例如, ‘categoryById’: 'SELECT * FROM categories WHERE id = :id’将生成方法。Drift 还支持自定义查询中的其他便捷功能,例如在 SQL 中嵌入 Dart 表达式。更多详情,请参阅漂移文件categoryById(int id)文档。
关于表名
要使用此功能,了解 Dart 表在 SQL 中的命名方式会很有帮助。对于未覆盖 的表tableName,SQL 中的名称将是snake_case类名的 。因此,名为 的 Dart 表Categories将被命名为categories,名为 的表UserAddressInformation将被命名为user_address_information。同样的规则也适用于没有明确名称的列 getter。Drift文件中声明的表和列将始终具有您指定的名称。
您也可以在此处使用UPDATEorDELETE语句。当然,此功能也适用于 daos,并且可以通过分析您正在读取或写入的表与自动更新流完美集成。
如果您不想使用生成的 API 中的语句,您仍然可以通过调用customSelect一次性查询或 customSelectStream查询流来发送自定义查询,当底层数据发生变化时,查询流会自动发出一组新的项目。使用 入门指南中介绍的待办事项示例,我们可以编写以下查询,它将加载每个类别中待办事项条目的数量:
class CategoryWithCount {
final Category category;
final int count; // amount of entries in this category
CategoryWithCount({required this.category, required this.count});
}
// then, in the database class:
Stream<List<CategoryWithCount>> allCategoriesWithCount() {
// select all categories and load how many associated entries there are for
// each category
return customSelect(
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount"'
' FROM categories c;',
// used for the stream: the stream will update when either table changes
readsFrom: {todoItems, categories},
).watch().map((rows) {
// we get list of rows here. We just have to turn the raw data from the
// row into a CategoryWithCount instnace. As we defined the Category table
// earlier, drift knows how to parse a category. The only thing left to do
// manually is extracting the amount.
return rows
.map((row) => CategoryWithCount(
category: categories.map(row.data),
count: row.read<int>('amount'),
))
.toList();
});
}
对于自定义选择,您应该使用readsFrom参数来指定查询从哪些表读取数据。使用 时Stream,漂移将能够知道在哪些更新之后流应该发出项目。
您还可以使用问号占位符和variables参数绑定 SQL 变量:
Stream<int> amountOfTodosInCategory(int id) {
return customSelect(
'SELECT COUNT(*) AS c FROM todo_items WHERE category = ?',
variables: [Variable.withInt(id)],
readsFrom: {todoItems},
).map((row) => row.read<int>('c')).watchSingle();
}
当然,您也可以使用索引变量(如?12) - 有关它们的更多信息,请参阅 sqlite3 文档。
对于更新和删除语句,您可以使用customUpdate。与 一样customSelect,该方法也接受 SQL 语句和可选变量。您还可以使用可选updates参数来指示哪些表将受查询影响。这将有助于其他选择流,这些流随后将自动更新。