表(Tables)
摘要
定义你的数据库结构。
由于 drift 是一个为关系型数据库构建的库,因此表(tables)是组织数据库的基本构建块。它们封装了特定的条目或概念,定义了存储数据的结构。对于每个表,drift 都会生成一个类型安全的行类,从而允许以高级 Dart 代码的形式编写查询和更新。本页列出了在声明表和列时可用的选项。
定义表
所有用 Drift 定义的表都遵循一个共同的结构来定义列:
- 每个表都定义为一个继承自
Table的 Dart 类。 - 在表类中,列被定义为
late final字段。 - 每个字段的开头(如
integer())决定了列的类型。
让我们再看一下入门示例中定义的表:
每个列的定义都必须以一对额外的括号结尾。如果你忘记了,Drift 会发出警告。
请注意,列默认是不可为空的。使用 nullable() 允许存储 null 值。
这里定义了两个表:todo_items,包含 id、title、category 和 created_at 列;以及 todo_category,包含 id 和 description 列。
这个表的 SQL 等价物是:
CREATE TABLE todo_items (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT,
created_at INTEGER -- Drift 默认将 dateTime() 列存储为 unix 时间戳
);一些技术说明:
- 表名
todo_items是从类名自动推导出来的。这可以通过重写tableNamegetter 来定制。更多信息请参见表名。 id列被自动设置为主键,因为它是一个自增整数。更多信息请参见主键。- 默认情况下,
dateTime()列存储为 Unix 时间戳。要将它们存储为 ISO-8601 字符串,请参见 DateTime 选项。
添加到数据库
通过将表添加到 @DriftDatabase 注解中,将它们添加到你的数据库中。
@DriftDatabase(tables: [TodoItems])
class Database extends _$Database {
Database(super.e);
@override
int get schemaVersion => 1;
}当你添加一个新表时,你必须再次运行代码生成器:
dart run build_runner build当数据库第一次打开时(通常是第一次运行应用程序时),Drift 会用所有定义的表初始化一个全新的数据库。
但是,如果数据库已经存在,Drift 不会自动对其结构进行任何更改。在这种情况下,请参阅迁移以了解如何更改数据库。
列类型
表中的每一列都有一个固定的类型,描述了它可以存储的值。Drift 提供了多种内置的列类型,以满足大多数数据库的需求。
| Dart 类型 | Drift 列 | SQL 类型1 |
|---|---|---|
int | late final age = integer()() | INTEGER |
BigInt (64位, 参见原因) | late final age = int64()() | INTEGER |
String | late final name = text()() | TEXT |
bool | late final isAdmin = boolean()() | INTEGER (1 或 0) |
double | late final height = real()() | REAL |
Uint8List | late final image = blob()() | BLOB |
DriftAny | late final value = sqliteAny()() | ANY (用于 STRICT 表) |
DateTime (参见选项) | late final createdAt = dateTime()() | INTEGER 或 TEXT |
| 你自己的类型 | 参见类型转换器文档。 | 取决于类型 |
| 枚举 | intEnum 或 textEnum | INTEGER 或 TEXT |
| Postgres 类型 | 参见 Postgres 文档。 | 取决于类型 |
除了这些基本类型之外,列还可以配置为存储任何可以转换为内置类型的类型。更多信息请参见类型转换器。
主键
数据库中的每个表都应该有一个主键——一个或一组唯一标识每一行的列。
单个自增键
对于大多数表来说,一个单个的自增整数列就足以作为主键。
在 Drift 中,这些列通过在列定义中使用 autoIncrement() 来声明,这将:
- 使该列成为表的唯一主键。因此,你不能在多个列上使用
autoIncrement(),也不能将autoIncrement()与其他主键混合使用。 - 使该列为每个新行自动加 1。
例如,在声明一个带有自增列的表时:
提示:使用 mixin 共享通用列
你可以将可能在多个表中需要的通用列定义提取到 Dart mixin 中:
dartmixin TableMixin on Table { // 主键列 late final id = integer().autoIncrement()(); // 创建时间戳列 late final createdAt = dateTime().withDefault(currentDateAndTime)(); } class Posts extends Table with TableMixin { late final content = text()(); }上面的
Posts表将包含来自TableMixin的id和createdAt列。
自定义主键
如果你需要一个不同的列(或一组列)作为主键,请在你的表类中重写 primaryKey getter。
- 它必须使用
=>语法定义,不支持函数体。 - 它必须返回一个集合字面量,不包含
if、for或展开运算符等集合元素。
class Profiles extends Table {
late final email = text()();
@override
Set<Column<Object>> get primaryKey => {email};
}上面的代码会将 email 列设置为主键。
定义列
在 Drift 中,列使用 late final 字段声明。该字段值的开头表示列的类型。额外的修饰符通过链式方法调用来表示。多个修饰符可以应用于同一列。
可空列
如果在一个列上调用此方法,它将能够存储 null 值。对于不可为空的列,Drift 在插入行时也会将相关参数标记为 required:
如果没有 nullable() 调用,age 将是一个必需的列。尝试在现有行中将此列设置为 null 将会抛出异常。
默认值
有些列不一定是可空的,但仍然有一个所有新行都可以共享的合理默认值。Drift 提供了两种指定默认值的方法:withDefault() 会向模式中的列添加一个 DEFAULT 约束(这在其他数据库框架中有时也称为“服务器默认值”)。clientDefault() 不会改变模式,而是在 Dart 中计算一个默认值,该值会隐式添加到 Drift 生成的插入语句中。
withDefault()
将默认值设置为一个在数据库本身中应用的 SQL 表达式。有关如何编写这些表达式的更多信息,请参见表达式。添加、删除或更改默认值被视为需要特别注意的模式更改。
一个常见的默认值示例是添加一个描述行创建时间的列:
尽管不可为空,但具有默认值的列对于插入操作不是 required 的,因为数据库将使用默认值作为后备。
clientDefault()
与 withDefault() 类似,这也为列设置了默认值。然而,与 withDefault() 不同的是,这个值是在 Dart 中计算的,而不是在数据库中 (1)。这意味着添加、删除或更改默认值不需要数据库迁移。但是,由于此默认值仅在你的 Dart 代码中应用,因此在 Drift 之外与数据库交互时不会应用它。
推荐
对于大多数用例,推荐使用
clientDefault而不是withDefault(),因为它提供了更大的灵活性,并且不需要数据库迁移。
引用
外键引用可以在 Dart 表中使用 references() 方法在构建列时表示:
references 的第一个参数指向应在其上创建引用的表。第二个参数是要用于引用的列的符号。
可选地,onUpdate 和 onDelete 参数可用于描述当目标行被更新或删除时应该发生什么。
请注意,在 sqlite3 中,默认情况下不启用外键引用。它们需要通过 PRAGMA foreign_keys = ON 来启用。在 Drift 中,一个合适的地方来发出该 pragma 是在迁移后回调中。
额外的验证检查
向列添加一个检查约束。如果在创建或更新行时此表达式的计算结果为 false,则会抛出异常。有关如何编写表达式的更多信息,请参见表达式。
检查约束和迁移
如果现有数据不满足检查约束,迁移将会失败。在添加检查约束之前,请确保它与现有数据兼容。
示例
确保 age 大于或等于 0。
注意
要使用像
isBiggerOrEqualValue这样的类型特定表达式,你必须显式定义列的类型。在上面的示例中,age列被显式定义为Column<int>。
约束文本长度
设置文本列的最小和/或最大长度。由于历史原因,此检查在 Dart 中执行(因此更改约束不需要迁移)。为了进行更强的-致性检查,请考虑改用检查约束。
示例
确保 name 不为空且长度小于 50 个字符:
生成列
使用 generatedAs 方法创建一个基于表中其他列计算的列。
与大多数数据库匹配,支持计算生成列和存储生成列:
默认情况下,生成列是虚拟的。虚拟列的值在每次查询时计算。
将 stored 参数设置为 true 以创建一个存储列。存储列的值计算一次,然后存储在数据库中。
唯一列
有时,列可能不是主键的一部分,但仍然已知包含唯一值。可以通过在模式中包含它来强制执行此唯一性,这可以加快某些查询的速度。要强制单个列在所有行中都是唯一的,请在其定义中使用 unique():
要强制列的组合是唯一的,请在你的表类中重写 uniqueKeys getter:
class Reservations extends Table {
late final reservationId = integer().autoIncrement()();
late final room = text()();
late final onDay = dateTime()();
@override
List<Set<Column>> get uniqueKeys => [
{room, onDay},
];
}上面的示例将强制同一天不能预订同一个房间两次。
对于主键不需要
主键在每个表中已经是唯一的,因此你不必为与主键匹配的列添加唯一约束。
索引
当一个不是主键或唯一键的列经常在 where 子句中用作过滤器时,可以使用索引来加快这些查询的速度。对于大型表尤其如此:没有索引,数据库引擎基本上必须遍历每一行才能找到与你的 where 子句匹配的行。对于每个索引,都会在幕后创建并维护一个将索引值映射到匹配行的查找结构。这使得数据库能够快速找到与查询匹配的行,而无需扫描整个表。
使用 @TableIndex 注解和你想要索引的列以及一个唯一的名称来标识索引来创建一个索引。unique 参数可以设置为 true 以强制索引列中的所有值都是唯一的。
要在一个表上创建多个索引,请添加多个 @TableIndex 注解。
注意
索引是为这些列自动创建的,不需要手动定义。
- 主键
- 唯一列
- 外键约束的目标列
示例
如果 users 表包含大量行,此索引将使基于用户名的查询更有效:
要为列指定排序模式,你可以使用一个 IndexedColumn 实例:
基于 SQL 的索引
如果你需要在索引中使用更多选项,例如定义部分索引,你也可以使用直接的 SQL 语句来定义你的索引:
正如你所期望的,Drift 将在构建时验证 CREATE INDEX 语句。
自定义约束
Drift 提供了专门的 API 来表示可以应用于 SQL 中表的最常用约束和选项。不直接支持的表约束仍然可以通过嵌入到 Drift 定义中的 SQL 片段来应用。
自定义列约束
类型化的列构建器 API 涵盖了大多数要设置在列上的约束。但是,如果你需要更具体的东西,你可以使用 customConstraint 方法将你自己的 SQL 约束应用于列:
自定义约束会替换 Drift 约束
添加
customConstraint会覆盖 Drift 添加的任何约束。最值得注意的是,它会删除NOT NULL约束。如果你想添加一个自定义约束并保持列为NOT NULL,你必须手动添加它。
示例:
如果你忘记包含 NOT NULL,或者尝试将自定义约束与不兼容的列选项混合使用,Drift 的构建器也会发出警告。
自定义表约束
你还可以通过在你的表类中重写 tableConstraints getter 来向表本身添加自定义约束。
class TableWithCustomConstraints extends Table {
late final foo = integer()();
late final bar = integer()();
@override
List<String> get customConstraints => [
'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)',
];
}SQL 验证
不用担心语法错误或不支持的功能。Drift 会验证你提供的 SQL,并在代码生成期间如果有任何问题,则会抛出错误。
STRICT 和 WITHOUT ROWID 表
SQLite 支持一种"strict" 表的概念,其中对列应用了更严格的类型检查规则。Drift 目前默认不启用此选项,但可能会在未来的主要版本中选择这样做。
通过重写 isStrict getter,可以使 Drift 定义的表变得严格:
同样,可以通过重写 withoutRowId getter 来创建 WITHOUT ROWID 表。
高级模式选项
更改 SQL 名称
默认情况下,Drift 将 Dart getter 名称转换为 snake_case 以确定在 SQL 中使用的列的名称。例如,在 Dart 中名为 createdAt 的列在 Drift 发出的 CREATE TABLE 语句中将被命名为 created_at。通过使用 named(),你可以显式设置列的名称:
只需要替代的大小写?
如果你只使用
named()来更改 Drift 在将 Dart 列名转换为 SQL 时使用的列的大小写,你可能希望改用全局的case_from_dart_to_sql构建器选项。除了snake_case(默认)之外,Drift 还支持以下大小写选项:
preservecamelCaseCONSTANT_CASEPascalCaselowercaseUPPERCASE通过在你的
build.yaml文件中设置case_from_dart_to_sql选项来自定义此项。yamltargets: $default: builders: drift_dev: options: case_from_dart_to_sql : snake_case # default
对于表,Drift 将其在 SQL 中的名称命名为类名的 snake_case 变体。可以通过在你的表类中重写 tableName getter 来定制一个表。
class Products extends Table {
@override
String get tableName => 'product_table';
}何时使用 BigInt 和 int64()
在 SQL 中,Drift 的 integer() 和 int64() 类型都映射到存储 64 位整数的列类型(在 SQLite 中为 INTEGER)。这意味着整数列的行为与原生 Dart 中的 int 的行为相匹配。然而,当编译为 JavaScript 时,我们遇到了一个问题:大值无法由 JavaScript 唯一的数字类型 64 位双精度浮点数精确表示。
因此,对于需要编译为 JavaScript 并且需要在整数列中存储可能很大的数字的项目,Drift 提供了 int64(),它在 Dart 中将所有数字表示为 BigInt,从而避免了与 JavaScript 的兼容性问题。
DateTime 选项
由于 SQLite 没有专门的类型来存储日期和时间值,Drift 提供了两种存储 DateTime 对象的方法:
- Unix 时间戳:数据库中
dateTime()列的列类型是INTEGER,存储以秒为单位的 unix 时间戳。不提供时区信息或亚秒级精度。 - ISO-8601 字符串(推荐):将
dateTime()列存储为文本。由于其更高的精度和时区感知能力,推荐用于大多数应用程序。
由于向后兼容性的原因,Drift 默认使用 Unix 时间戳。但是,我们建议新项目使用 ISO-8601 字符串。要启用此功能,请在你的 build.yaml 文件中调整 store_date_time_values_as_text 选项:
targets:
$default:
builders:
drift_dev:
options:
store_date_time_values_as_text: false # (default)
# To use ISO 8601 strings
# store_date_time_values_as_text: true有关日期如何存储以及如何在存储方法之间切换的更多信息,请参见 DateTime 迁移指南。
- SQL 类型仅在数据库中使用。JSON 序列化不受 SQL 类型的影响。例如,
bool值在 JSON 中序列化为true或false,即使它们在数据库中存储为1或0。 ↩