更新/插入

说明

upsert() 是一个数据操纵语言 (DML) 阶段,用于更新现有文档或插入新文档(如果不存在)。此阶段可以附加到查询的末尾。

upsert() 阶段可以接受不同的参数,以确定目标集合、文档 ID 和可选字段转换:

  • upsert(collectionName, documentIdExpression, ...transformations):将文档 upsert 到指定集合中,使用求值表达式确定文档 ID,并应用可选转换。
  • upsert(collectionName, ...transformations):将文档 upsert 到指定集合中,要求输入文档中包含 __name__ 字段以确定文档 ID,并应用可选转换。
  • upsert(...transformations):更新插入文档,要求输入文档中包含 __name__ 字段以确定目标集合和文档 ID,并应用可选转换。

示例

迁移活跃用户

以下示例查询 users 集合(其中 age >= 25),添加 processed 字段,并使用 name 字段作为文档 ID upsert 到 new_users 中:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("users") .where(field("age").greaterThanOrEqual(25)) .addFields(constant(true).as("processed")) .upsert({ collection: "new_users", documentId: field("name") }) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("users") .where(field("age").greaterThanOrEqual(25)) .addFields(constant(true).as("processed")) .upsert(new Upsert().withCollection("new_users").withDocumentId(field("name"))); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

行为

以下部分介绍了 upsert() 阶段的执行行为、响应、转换和限制。

使用字段转换就地 upsert

以下示例通过在 upsert 期间修改或添加字段,就地更新 users 集合中的现有文档:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("users") .upsert([constant(31).as("age")]) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("users") .upsert(constant(31L).as("age")); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

使用显式 ID 插入到其他集合中

以下示例从 source_coll 中读取数据,并使用显式文档 ID "new_doc_id" 将文档 upsert 到 target_coll 中,同时应用字段转换:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("source_coll") .upsert( [constant(31).as("age")], { collection: "target_coll", documentId: constant("new_doc_id") } ) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("source_coll") .upsert(new Upsert(constant(31L).as("age")) .withCollection("target_coll") .withDocumentId(constant("new_doc_id"))); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

使用现有文档 ID (__name__) upsert 到另一个集合中

以下示例从 source_coll 读取文档,并使用每个文档的现有文档 ID (__name__) 将其 upsert 到 target_coll 中:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("source_coll") .upsert([constant("updated_val").as("status")], { collection: "target_coll" }) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("source_coll") .upsert(new Upsert(constant("updated_val").as("status")) .withCollection("target_coll")); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

使用文档引用进行 upsert (__name__)

以下示例使用分配给 __name__ 字段的文档引用来指定目标集合和文档 ID:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .literals([ { __name__: db.doc("users/bob"), name: "bob", age: 30 }, { __name__: db.doc("users/alice"), name: "alice", age: 25 } ]) .upsert() ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .literals( Map.of("__name__", db.document("users/bob"), "name", "bob", "age", 30), Map.of("__name__", db.document("users/alice"), "name", "alice", "age", 25)) .upsert(new Upsert()); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

使用自动生成的文档 ID 进行 upsert 操作

以下示例使用自动生成的文档 ID 和字段转换,将字面量 upsert 到 users 集合中:

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .literals([ { foo: "bar" }, { foo: "baz" } ]) .upsert([constant("active").as("status")], { collection: "users" }) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .literals( Map.of("foo", "bar"), Map.of("foo", "baz")) .upsert(new Upsert(constant("active").as("status")) .withCollection("users")); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

Upsert 到子集合

upsert() 阶段可用于使用完整路径直接将文档插入或更新到子集合中。

Node.js
```js const results = await db.pipeline() .literals([ { title: "post1" }, { title: "post2" } ]) .upsert([], { collection: "users/u1/posts" }) .execute(); ```
Java
```java Pipeline pipeline = db.pipeline() .literals( Map.of("title", "post1"), Map.of("title", "post2")) .upsert(new Upsert().withCollection("users/u1/posts")); pipeline.execute(); ```

通过高级查询(搜索和查找最近邻)进行 Upsert

upsert() 阶段可与 search 或 findNearest 等高级查询操作搭配使用,以将匹配的文档写入输出集合或就地更新。

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("products") .search({ query: "shoes" }) .upsert([constant(true).as("is_shoes")]) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("products") .search(searchOptionsBuilder().query(documentMatches("shoes"))) .upsert(new Upsert(constant(true).as("is_shoes"))); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

从聚合子查询中执行 upsert 操作

upsert() 阶段可与聚合子查询和标量函数结合使用,以根据动态计算的值更新文档,例如确定用户是否获得了最高分。此模式必须在用户事务中执行。

Node.js
```js const results = await db.runTransaction(async (transaction) => { return transaction.execute( db.pipeline() .collection("users") .where( field("score").equal( scalar( db.pipeline() .collection("scores") .aggregate({ accumulators: [max(field("points")).as("max_points")] }) .select(field("max_points")) ) ) ) .upsert([constant(true).as("is_top_scorer")]) ); }); ```
Java
```java Pipeline pipeline = db.pipeline() .collection("users") .where( eq( field("score"), scalar( db.pipeline() .collection("scores") .aggregate(max(field("points")).as("max_points")) .select(field("max_points"))))) .upsert(new Upsert(constant(true).as("is_top_scorer"))); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

回答

upsert() 阶段始终会发出一个文档(如 { documents_modified: 3 }),用于描述插入或更新了多少个文档。

最终阶段

upsert() 阶段必须位于流水线的末尾,不能再提供其他阶段。

需要事务

upsert() 阶段需要事务。它必须在活跃的读写事务中运行,或者在启动新事务时将 atomic (Node.js) 或 auto_commit 选项设置为 true。

回答

响应包含已修改文档数量的摘要。 例如,以下响应确认流水线修改了三个文档:

{documents_modified: 3L}

限制

  • 现在可以在事务中运行 DML 阶段。在大多数情况下,insert() 和 upsert() 阶段需要在事务中运行。

  • 在新事务中执行具有 DML 阶段的流水线时,您可以使用 atomic 选项在事务中自动运行,并在流水线成功执行后提交。

  • 在事务中运行时,其他阶段(例如 where、addFields、union、findNearest 和 aggregate 子查询)没有任何限制。事务性 DML 操作受 Firestore 企业版事务限制的约束。由于事务不允许在首次写入后进行读取,因此您无法在单个事务中多次调用 DML 流水线,也无法在 DML 流水线执行后读取文档。在 DML 写入后尝试执行读取操作会失败,并显示 FAILED_PRECONDITION 错误:Cannot have reads after the first write in a Firestore transaction.

  • 在事务之外运行时:

    • 每个文档都是独立更新的,并且跨文档的写入操作不具有原子性。流水线会在遇到第一个错误时停止,从而导致部分成功。
    • 不支持高级多查询阶段(aggregate、distinct、unnest、find_nearest、union、联接和子查询)。
  • 在 Firebase 安全规则下,每批文档都会重新评估规则并执行查找。如果规则使用 get() 或 exists(),则在写入大约 200 个文档后,会达到全局 20 次查找限制,并且操作会失败,并显示 PERMISSION_DENIED。请使用自定义声明,而不是 get() / exists() 查找,以避免此限制。

  • 如果 DML 阶段之前的阶段生成了多个具有相同 __name__ 的文档,或计算结果为相同的目标文档 ID,则每个变更都会按顺序处理:

    • 对于 upsert(...) 和 update(...):针对同一文档的多个变更遵循“最后一次变更生效”语义,即最后处理的变更决定文档的最终状态。
    • 对于 delete(...):第一次尝试会删除文档,而对同一文档的后续尝试则不执行任何操作。
    • 对于 insert(...):第一次尝试会创建文档,而后续针对同一文档 ID 的尝试会失败,并显示 ALREADY_EXISTS 错误。

事务性执行

DML 操作可以作为读写事务的一部分执行,方法是向流水线执行调用提供显式事务,或使用 atomic 选项。

Firestore 事务要求所有读取操作在任何写入操作之前执行。由于 DML 流水线会执行写入操作,因此在同一事务中运行 DML 流水线后,您无法执行读取操作(例如文档查找或查询读取)。例如,您不能在单个事务中多次调用 DML 流水线。在 DML 流水线执行后尝试读取失败,并显示 FAILED_PRECONDITION 错误:Cannot have reads after the first write in a Firestore transaction.

使用显式事务

Node.js
```js // Execute a pipeline with DML stage within an explicit transaction const pipeline = db.pipeline() .collection("users") .where(field("active").equal(false)) .addFields(constant("inactive").as("status")) .update(); await db.runTransaction(async (transaction) => { return transaction.execute(pipeline); }); ```
Java
```java // Execute a pipeline with DML stage within an explicit transaction Pipeline pipeline = db.pipeline() .collection("users") .where(field("active").equal(false)) .addFields(constant("inactive").as("status")) .update(); db.runTransaction(transaction -> { transaction.execute(pipeline); return null; }); ```

使用 atomic 选项

Node.js
```js // 执行流水线 const results = await db.pipeline() .collection("users") .where(field("active").equal(false)) .addFields(constant("inactive").as("status")) .update() .execute({ atomic: true }); ```
Java
```java // Execute a pipeline passing atomic option to execute() Pipeline pipeline = db.pipeline() .collection("users") .where(field("active").equal(false)) .addFields(constant("inactive").as("status")) .update(); pipeline.execute(new PipelineExecuteOptions().withAtomic(true)); ```