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 面板檢視(Panel view)中每份文件的 ⋮ 選單裡加入變更文件 ID。它會以一次寫入把文件及其子集合中的所有內容移到新的 ID,所以要嘛全部完成,要嘛都不做。它用你連結的 Google 帳戶寫入,所以不需要設定服務帳戶金鑰,也不需要寫程式碼。

  1. 從 Chrome 線上應用程式商店安裝 Firexport。
  2. 在 Firebase 控制台開啟 Firestore Database > 資料(Data)的面板檢視(Panel view),然後按一下文件所在的集合。
  3. 在清單中將滑鼠游標移到該文件上,按一下 ID 左側出現的 ⋮,再按一下變更文件 ID。
  4. 第一次使用時,視窗會要求你連結 Google 帳戶:選擇已登入控制台的帳戶,並在 Google 的視窗中允許存取。
  5. 輸入新的文件 ID,然後按一下變更 ID。沒有子集合的文件會立即移動。
  6. 如果文件有子集合,Firexport 會先讀取它們,並顯示將移動多少份文件、需要多少次寫入,以及檢查時用了多少次讀取。按一下移動。接著控制台會開啟新 ID 下的這份文件。

哪些情況下不會移動

  • 新的 ID(或其下)已有文件。Firexport 從不覆寫:這次寫入會被拒絕,不會有任何變更。
  • Firexport 讀取之後,其中任何一份文件有了變更。否則舊的內容會被寫到新的 ID 下,新的內容卻被刪除,所以不會寫入任何資料,你可以再試一次。
  • 超過 250 份文件,或資料超過一次寫入能承載的上限(10 MiB)。Firexport 會在寫入任何資料之前停止;請改用方法 2。
  • Firestore 不允許的 ID:含有 /、整個 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 次刪除的配額中扣除。