Skip to content

写入(更新、插入、删除)

摘要

在 Dart 中从表中选择行或单个列。


更新和删除

你可以使用生成的类来更新任何行的单个字段:

dart
Future moveImportantTasksIntoCategory(Category target) {
  // for updates, we use the "companion" version of a generated class. This wraps the
  // fields in a "Value" type which can be set to be absent using "Value.absent()". This
  // allows us to separate between "SET category = NULL" (`category: Value(null)`) and not
  // updating the category at all: `category: Value.absent()`.
  return (update(todos)
      ..where((t) => t.title.like('%Important%'))
    ).write(TodosCompanion(
      category: Value(target.id),
    ),
  );
}

Future updateTodo(Todo entry) {
  // using replace will update all fields from the entry that are not marked as a primary key.
  // it will also make sure that only the entry with the same primary key will be updated.
  // Here, this means that the row that has the same id as entry will be updated to reflect
  // the entry's title, content and category. As its where clause is set automatically, it
  // cannot be used together with where.
  return update(todos).replace(entry);
}

Future feelingLazy() {
  // delete the oldest nine tasks
  return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
}

⚠️ 警告: 如果你没有在更新或删除上显式添加 where 子句,该语句将影响表中的所有行!

条目、伴生对象 - 为什么我们需要所有这些?

你可能已经注意到,我们在第一次更新时使用了 TodosCompanion,而不是仅仅传递一个 Todo。Drift 生成 Todo 类(也称为表的_数据类_)来保存一个包含其所有数据的完整行。对于_部分_数据,请优先使用伴生对象。在上面的示例中,我们只设置了 category 列,所以我们使用了伴生对象。为什么这是必要的?如果一个字段被设置为 null,我们将不知道是需要在数据库中将该列设置回 null,还是应该让它保持不变。伴生对象中的字段有一个特殊的 Value.absent() 状态,这使得这一点变得明确。

伴生对象还有一个用于插入的特殊构造函数 - 所有没有默认值且不可为空的列在该构造函数上都标记为 @required。这使得伴生对象更容易用于插入,因为你知道要设置哪些字段。

使用 SQL 表达式进行更新

在某些情况下,你可能希望根据许多行的当前值来更新它们。一种选择是首先将受影响的行选择到 Dart 对象中,根据这些结果创建伴生对象,并使用它们进行更新。如果更新可以用 SQL 描述,那么使用 Companion.custom 可以提供一种更有效的方法:

dart
await db
    .update(db.users)
    .write(UsersCompanion.custom(username: db.users.username.lower()));

在这里,users 表中的 name 列对于所有现有行都更改为小写。由于列上的 .lower() 是在数据库中实现的,因此在语句执行期间不必将行加载到 Dart 中。

插入

你可以非常容易地将任何有效的对象插入到表中。由于某些值可能不存在(例如我们不必显式设置的默认值),我们再次使用伴生版本。

dart
// returns the generated id
Future<int> addTodo(TodosCompanion entry) {
  return into(todos).insert(entry);
}

所有生成的行类都将有一个可用于创建对象的构造函数:

dart
addTodo(
  TodosCompanion(
    title: Value('Important task'),
    content: Value('Refactor persistence code'),
  ),
);

如果一列是可空的或有默认值(这包括自增),则可以省略该字段。所有其他字段都必须设置且不为 null。否则 insert 方法将抛出异常。

通过使用批处理,可以有效地运行多个插入语句。为此,你可以在 batch 中使用 insertAll 方法:

dart
Future<void> insertMultipleEntries() async{
  await batch((batch) {
    // functions in a batch don't have to be awaited - just
    // await the whole batch afterwards.
    batch.insertAll(todos, [
      TodosCompanion.insert(
        title: 'First entry',
        content: 'My content',
      ),
      TodosCompanion.insert(
        title: 'Another entry',
        content: 'More content',
        // columns that aren't required for inserts are still wrapped in a Value:
        category: Value(3),
      ),
      // ...
    ]);
  });
}

批处理在所有更新都原子化地发生的意义上类似于事务,但它们启用了进一步的优化以避免两次准备相同的 SQL 语句。这使得它们适用于批量插入或更新操作。

Upserts

Upserts 是较新 sqlite3 版本的一项功能,它允许在已存在冲突行的情况下,插入操作的行为类似于更新操作。

这允许我们在其主键是其数据的一部分时创建或覆盖现有行:

dart
class Users extends Table {
  TextColumn get email => text()();
  TextColumn get name => text()();

  @override
  Set<Column> get primaryKey => {email};
}

Future<int> createOrUpdateUser(User user) {
  return into(users).insertOnConflictUpdate(user);
}

当使用已存在的电子邮件地址调用 createOrUpdateUser() 时,该用户的姓名将被更新。否则,将在数据库中插入一个新用户。

插入也可以用于更高级的查询。例如,假设我们正在构建一个字典,并希望跟踪我们遇到一个单词的次数。一个用于此的表可能看起来像

通过使用自定义 upsert,我们可以插入一个新单词,或者如果它已经存在,则增加其 usages 计数器:

dart
Future<void> trackWord(String word) {
  return into(words).insert(
    WordsCompanion.insert(word: word),
    onConflict: DoUpdate(
      (old) => WordsCompanion.custom(usages: old.usages + Constant(1)),
    ),
  );
}

唯一约束和冲突目标

insertOnConflictUpdateonConflict: DoUpdate 都在 sql 中使用 DO UPDATE upsert。这要求我们提供一个所谓的“冲突目标”,即一组用于检查唯一性冲突的列。默认情况下,drift 将使用表的主键作为冲突目标。这在大多数情况下都有效,但是如果你在某些列上有自定义的 UNIQUE 约束,你需要在 Dart 的 DoUpdate 上使用 target 参数来包含这些列:

dart
class MatchResults extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get teamA => text()();
  TextColumn get teamB => text()();
  BoolColumn get teamAWon => boolean()();

  @override
  List<Set<Column<Object>>>? get uniqueKeys => [
    {teamA, teamB},
  ];
}
Future<void> insertMatch(String teamA, String teamB, bool teamAWon) {
  final data = MatchResultsCompanion.insert(
    teamA: teamA,
    teamB: teamB,
    teamAWon: teamAWon,
  );

  return into(matches).insert(
    data,
    onConflict: DoUpdate(
      (old) => data,
      target: [matches.teamA, matches.teamB],
    ),
  );
}

请注意,这需要一个相当新的 sqlite3 版本(3.24.0),在使用 drift_sqflite 时,在较旧的 Android 设备上可能不可用。NativeDatabasessqlite3_flutter_libs 在 Android 上包含最新的 sqlite,因此如果你想支持 upsert,请考虑使用它。

另请注意,当发生 upsert 时,返回的 rowid 可能不准确。

Returning

你可以使用 insertReturning 插入一行或伴生对象,并立即获取它插入的行。返回的行包含所有生成的默认值和自增 id。

注意: 这使用了 sqlite3 版本 3.35 中添加的 RETURNING 语法,默认情况下在大多数操作系统上都不可用。使用此方法时,请确保你有可用的最新 sqlite3 版本。sqlite3_flutter_libs 就是这种情况。

例如,考虑使用入门指南中的表的此代码段:

dart
final row = await into(todos).insertReturning(TodosCompanion.insert(
  title: 'A todo entry',
  content: 'A description',
));

返回的 row 设置了正确的 id。如果一个表有更多的默认值,包括像 CURRENT_TIME 这样的动态值,那么这些值也将在 insertReturning 返回的行中设置。