Firexport指南

如何重命名 Firestore 文档(更改文档 ID)

Firestore 无法重命名文档。文档 ID 是其路径的一部分,Firebase 控制台和各个 SDK 都没有更改它的方法。所谓重命名,就是在新 ID 下重新创建这个文档,再删除旧文档,而且其子集合中的所有内容也要一并处理,因为删除文档并不会动到子集合。有三种方法可以做到,哪一种合适,取决于文档下面有多少内容。

更新于 2026年10月27日。 方法 1 中的扩展程序 Firexport 是我们开发的。方法 2 和 3 不需要它。

简短回答

  • 现在就要重命名一个文档,连同它的子集合: 用浏览器扩展程序在控制台的面板视图(Panel view)中更改 ID。 方法 1
  • 文档下面有超过 250 个文档,或者要重命名许多文档: 一段简短的 Admin SDK 脚本。 方法 2
  • 一个没有子集合的小文档,而且什么都不想安装: 在控制台中手动复制。 方法 3
1. 控制台 + 扩展程序2. Admin SDK 脚本3. 在控制台中手动操作
准备工作安装一个 Chrome 扩展程序,连接你的 Google 账号Node.js 和 Google Cloud 凭据无
子集合一起移动,包括所有层级一起移动,包括所有层级留在原处
字段值原样保留保留,但像 2.0 这样的 double 会变成整数重新输入
全有或全无是,一次写入是,一个批次(最多 10 MiB)否
规模最多 250 个文档无限制几个字段

1. 用 Firexport 在 Firebase 控制台中更改 ID

Firexport 是一款免费的 Chrome 扩展程序,会在 Firestore 的面板视图中每个文档的 ⋮ 菜单里添加更改文档 ID。它用一次写入把文档及其子集合中的所有内容移到新 ID,所以要么全部完成,要么都不做。它使用你连接的 Google 账号写入,因此无需设置服务账号密钥,也无需编写代码。

  1. 从 Chrome 应用商店安装 Firexport。
  2. 在 Firebase 控制台中,以面板视图(Panel view)打开 Firestore Database > 数据(Data),然后点击文档所在的集合。
  3. 把鼠标悬停在列表中的文档上,点击其 ID 左侧出现的 ⋮,然后选择更改文档 ID。
  4. 第一次使用时,窗口会要求你连接 Google 账号:选择在控制台中登录的账号,并在 Google 的窗口中允许访问。
  5. 输入新文档 ID,然后点击更改 ID。没有子集合的文档会立即移动。
  6. 如果文档有子集合,Firexport 会先读取它们,并显示将移动多少个文档、需要多少次写入,以及检查时用了多少次读取。点击移动。随后控制台会打开新 ID 下的这个文档。

哪些情况会阻止移动

  • 新 ID 上或其下已经有文档。Firexport 从不覆盖:这次写入会被拒绝,不会更改任何内容。
  • Firexport 读取之后,其中任何一个文档发生了变化。否则旧内容会落到新 ID 下,而新内容会被删除,所以不会写入任何内容,你可以重试。
  • 超过 250 个文档,或超过一次写入能容纳的上限(10 MiB)。Firexport 会在写入任何内容之前停止;请使用方法 2。
  • Firestore 不允许的 ID:包含 /,恰好是 . 或 ..,以 __ 开头和结尾,或者超过 1,500 字节。两端的空格也是 ID 的一部分,所以输入框会保留它们。

每个字段都会原样保留其值和类型,因为 Firexport 写入的是它从 Firestore 读取的内容,而不是控制台显示的内容。写入使用所连接账号的权限,因此不受安全规则约束。文档从你的浏览器直接发送到 Google,从不发送给我们。更改文档 ID 功能在所有方案中都免费。

直接在控制台中更改 Firestore 文档的 ID,连同子集合一起,只需一次写入。

添加到 Chrome — 免费

2. 用 Admin SDK 脚本移动

如果文档下面有超过 250 个文档,或者要重命名许多文档,一段使用 Firebase Admin SDK 的 Node.js 脚本可以完成同样的移动。下面的脚本会读取这个文档及其下的每个子集合,然后在新路径下逐一创建它们,再删除旧文档。新路径也可以位于另一个集合中,所以同一个脚本也能用来移动文档。它需要一个能写入 Firestore 的账号:项目 Owner 或 Editor,或者 Cloud Datastore User 角色。

  1. 安装 SDK:npm install firebase-admin
  2. 登录以获取本地凭据:gcloud auth application-default login。在服务器上,改为让 GOOGLE_APPLICATION_CREDENTIALS 指向服务账号密钥。
  3. 把下面的脚本保存为 rename-document.mjs,替换 your-project-id,然后运行 node rename-document.mjs users/alice users/alice-smith。
// Usage: node rename-document.mjs users/alice users/alice-smith   → moves users/alice and everything under it
// Firestore has no rename: this creates the document at the new path with all its subcollections, then deletes the old ones.
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

initializeApp({ projectId: 'your-project-id' });
const db = getFirestore();
db.settings({ useBigInt: true }); // integers past 2^53 would otherwise lose their last digits on the way through
const [from, to] = process.argv.slice(2);
if (!from || !to) throw new Error('Usage: node rename-document.mjs <document path> <new document path>');
if (to === from || to.startsWith(`${from}/`)) throw new Error('The new path must be outside the document you move');

// true: one batch, so every write happens or none does (up to 10 MiB of data in all).
// false: for more than that. Copies everything first and deletes the old documents only once every copy is written.
const ONE_BATCH = true;

// The document and every document under it, in subcollections of subcollections too. listDocuments() also returns
// places that have subcollections but no fields, so the walk reaches what is under them; the places themselves aren't copied.
async function collect(ref, found) {
  const snapshot = await ref.get();
  if (snapshot.exists) found.push(snapshot);
  for (const collection of await ref.listCollections()) {
    for (const child of await collection.listDocuments()) await collect(child, found);
  }
  return found;
}

const docs = await collect(db.doc(from), []);
if (docs[0]?.ref.path !== from) throw new Error(`${from} doesn't exist`);
const target = (doc) => db.doc(to + doc.ref.path.slice(from.length));

if (ONE_BATCH) {
  // create() fails if a document is already at the new path, and lastUpdateTime fails if one changed after it was read.
  // Either way Firestore rejects the whole batch and nothing is written.
  const batch = db.batch();
  for (const doc of docs) batch.create(target(doc), doc.data());
  for (const doc of docs) batch.delete(doc.ref, { lastUpdateTime: doc.updateTime });
  await batch.commit();
} else {
  // BulkWriter keeps under Firestore's rate limits and retries what can be retried, but it isn't all or nothing.
  const writer = db.bulkWriter();
  const failures = [];
  for (const doc of docs) {
    writer.create(target(doc), doc.data()).catch((error) => failures.push(`${doc.ref.path}: ${error.message}`));
  }
  await writer.flush();
  if (failures.length) {
    failures.forEach((failure) => console.log(failure));
    throw new Error('Some copies failed, so nothing was deleted. Delete the copies that were written, then try again.');
  }
  for (const doc of docs) {
    writer.delete(doc.ref, { lastUpdateTime: doc.updateTime }).catch((error) => failures.push(`${doc.ref.path}: ${error.message}`));
  }
  await writer.close();
  failures.forEach((failure) => console.log(`Not deleted, ${failure}`));
}
console.log(`Moved ${docs.length} documents from ${from} to ${to}`);
  • listDocuments() 也会返回只有子集合、自身没有字段的位置,也就是控制台中以斜体显示的 ID,所以脚本能找到它下面的文档。这个位置本身不会被复制:它的子集合出现在新路径下之后,它也就随之出现。
  • 开启 ONE_BATCH 时,所有创建和删除都放在一个批次中。如果新路径上已有文档,或者某个文档在脚本读取后发生了变化,Firestore 会拒绝这个批次,不会写入任何内容。
  • 一个批次最多可容纳 10 MiB。文档树更大时,把 ONE_BATCH 设为 false:脚本会用 BulkWriter 复制所有内容,并且只有在每个副本都写入成功后才删除旧文档。这种方式不能保证要么全部完成、要么都不做,所以在脚本运行期间不要写入这些文档。
  • 与方法 1 一样,它会绕过安全规则。时间戳、引用、地理位置和字节都保留各自的类型,useBigInt 会让超过 253 的整数保持精确。有一点是 Admin SDK 无法保留的:没有小数部分的 double 值(例如 2.0)会被读取为普通数字,并作为整数 2 写回。

3. 在控制台中手动复制

对于只有几个字段、没有子集合的文档,仅用控制台就够了。这需要两次互不关联的写入,而且每个值都要重新输入。

  1. 打开文档,记下每个字段的名称、类型和值。
  2. 在同一个集合中,点击添加文档(Add document),在文档 ID(Document ID)中输入新 ID,然后添加相同的字段,类型也要相同。点击保存(Save)。
  3. 检查新文档,然后打开旧文档的 ⋮ 菜单,点击删除文档(Delete document)。

操作时要留意类型:在字符串字段中输入的数字仍然是字符串,时间戳也必须以时间戳类型重新输入。子集合不会跟着过来,每个子集合都得用同样的方式复制,一次一个文档。

新 ID 会带来哪些变化

无论用哪种方式移动,新 ID 下的文档都是一个新文档。由此会带来几点变化。

  • 创建时间。每个被移动的文档都会获得新的创建时间和更新时间。
  • 引用。其他文档中的引用字段仍然指向旧路径,以字符串形式存储的 ID(例如 ownerId)也是如此。请自行更新它们。
  • Cloud Functions 和监听器。每个被移动的文档都会触发创建和删除触发器,监听该集合的应用会看到一个文档被移除、另一个文档被添加。
  • 费用。每个被移动的文档在账单上计为一次写入和一次删除,另加查找和读取它所需的读取次数。

常见问题

可以在 Firestore 中重命名文档吗?

不能直接重命名。Firestore 没有重命名操作,文档创建后其 ID 就无法更改。要重命名文档,需要用新 ID 创建一个数据相同的文档,再删除旧文档。Firexport 的“更改文档 ID”和上面的 Admin SDK 脚本会在一次写入中完成这两步。

重命名文档会移动它的子集合吗?

只有你把子集合也一起移动时才会。子集合属于文档的路径,而不是文档的数据,所以复制字段时子集合会被留下,删除旧文档也不会删除它们。Firexport 和上面的脚本会把所有层级的每个子集合与文档一起移动。

可以在 Firestore 中重命名集合吗?

不能。集合只因其中的文档而存在,所以重命名集合意味着把每个文档都移到新的集合名下。控制台和 Firexport 都无法一步完成;对每个文档执行上述移动的脚本可以做到。

指向旧文档的引用会怎样?

它们会继续指向旧路径,而移动之后这个路径已不存在。Firestore 不会更新其他文档中存储的引用字段或 ID,所以重命名之后请找到并更新它们。

重命名 Firestore 文档要花多少钱?

每个被移动的文档计为一次写入和一次删除,另加查找它及其子集合所需的读取。Firexport 会在你点击“移动”之前显示具体次数。在免费的 Spark 方案中,这会占用每天 20,000 次写入和 20,000 次删除的免费配额。