登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

IndexedDB 怎样迁移对象仓库结构

来源:17golang原创

时间:2026-11-08 05:06:09 303浏览 收藏

IndexedDB 的对象仓库结构不能像普通 JavaScript 对象那样随时改写。需要新增仓库、删除仓库或调整索引时,应提升数据库版本,让浏览器触发 upgradeneeded,再在这个版本变更事务中完成结构迁移。

官方参考:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB

把每次结构变化当成一个递增版本:新增结构直接创建,索引变化在升级事务中调整,keyPath 变化则先把旧数据读出,再重建仓库并写回;同时处理旧页面连接导致的 blocked。

先把结构变化拆成可迁移的版本

IndexedDB 用数据库版本号判断是否需要升级。首次打开数据库,或用比当前版本更大的版本号调用 indexedDB.open() 时,打开请求会触发 upgradeneeded。对象仓库和索引的创建、删除只能放在这个版本变更事务里,不能在普通读写事务中临时修改结构。

因此,不要把“当前应该有哪些仓库”写成每次启动都执行的无条件脚本,而要把变化拆成 oldVersion 到目标版本的几个区间。这样用户即使从很老的版本直接升级,也能按顺序补齐中间结构。

IndexedDB 数据库版本、对象仓库、索引与升级事务的静态关系说明图
图1:IndexedDB 结构迁移关系说明图,查看版本号、对象仓库、索引与升级事务的静态关系;这是说明图,不是运行截图。
// 用递增版本号承载结构变化,避免每次启动都重复创建仓库。
const request = indexedDB.open("notes-db", 3);

request.onupgradeneeded = (event) => {
  // oldVersion 表示升级前版本,newVersion 是本次目标版本。
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion  {
  // 打开成功后才把连接交给业务层,普通读写从这里开始。
  const db = event.target.result;
  console.log("database opened", db.version);
};

这里的判断重点是“升级前版本小于某个版本”,而不是只判断当前版本是否等于某个值。用户可能从 1 直接跳到 3,版本 2 的索引也必须被补上。对新仓库和新索引使用名称检查,还能让同一升级逻辑在已完成部分结构的数据库上更稳妥。

在 onupgradeneeded 中按版本执行变更

新增对象仓库最简单:提高版本号,在 onupgradeneeded 中调用 createObjectStore()。删除仓库则使用 deleteObjectStore()。已有仓库的索引同样应在升级事务中创建或删除,业务读写事务只负责数据,不负责改变模式。

可以把升级逻辑写成一个小型迁移表,每个版本只关心自己引入的变化。不要在每次打开时无条件调用 createObjectStore(),因为名称已经存在时会抛出异常,使升级事务失败。

// 每个版本块只声明新增的结构,已存在的名称不重复创建。
function migrateSchema(event) {
  const db = event.target.result;
  const tx = event.target.transaction;
  const oldVersion = event.oldVersion;

  if (oldVersion  {
  // 升级事务失败时不要假装迁移完成,把错误交给上层记录。
  console.error("IndexedDB schema upgrade failed", request.error);
};

删除仓库是破坏性动作,只有在确认数据已不再需要,或数据已经被搬到新仓库后才使用。索引变化通常不会改变仓库中的记录,但创建唯一索引时,如果旧数据存在重复值,升级事务仍可能失败,所以唯一性规则要先与旧数据兼容。

遇到 keyPath 变化时重建并搬运数据

keyPath 是对象仓库的结构属性,不能通过一个“修改选项”的调用直接替换。需要改变主键路径时,常见做法是在升级事务中读取旧仓库记录,创建带新 keyPath 的临时仓库,把转换后的记录写入临时仓库,再删除旧仓库并把临时仓库改成最终名称。

迁移数据时要注意:对象仓库的删除会一并删除其中的数据;迁移事务中任何一个请求失败,都可能让整个版本升级回滚。因此转换函数要明确处理缺失字段,并让异常自然进入事务失败路径。

// 将 notes 的主键从自增 id 迁移到稳定的 noteKey。
function rebuildNotesStore(event) {
  const db = event.target.result;
  const tx = event.target.transaction;
  const oldStore = tx.objectStore("notes");
  const cursorRequest = oldStore.openCursor();

  // 临时仓库使用新 keyPath,避免直接覆盖旧数据。
  const nextStore = db.createObjectStore("notes-v2", { keyPath: "noteKey" });
  nextStore.createIndex("by-updated-at", "updatedAt", { unique: false });

  cursorRequest.onsuccess = (cursorEvent) => {
    const cursor = cursorEvent.target.result;
    if (!cursor) {
      // 游标耗尽后再删除旧仓库,保证此前记录已经逐条写入临时仓库。
      db.deleteObjectStore("notes");
      return;
    }

    const oldRecord = cursor.value;
    if (typeof oldRecord.noteKey !== "string" || oldRecord.noteKey === "") {
      // 缺少新主键时抛错,让升级事务回滚,而不是静默丢弃记录。
      throw new Error("noteKey is required during migration");
    }
    nextStore.put({ ...oldRecord, noteKey: oldRecord.noteKey });
    cursor.continue();
  };
}

const request = indexedDB.open("notes-db", 6);
request.onupgradeneeded = (event) => {
  // 只有从 5 以下升级时才执行一次 keyPath 重建。
  if (event.oldVersion 

这里是迁移模型而不是可直接套用的业务代码:临时仓库的名字、字段转换规则和索引都要按项目数据设计。更稳妥的做法是先确认所有旧记录都能生成新主键,并把失败原因记录出来;不要为了“让升级通过”而跳过无法转换的记录。

处理旧页面连接和失败回退

当一个页面已经打开旧版本数据库,另一个页面尝试升级时,旧连接可能让升级请求进入 blocked。新页面应监听 blocked,旧页面应监听数据库连接的 versionchange 并及时关闭连接,否则升级没有机会取得版本变更锁。

IndexedDB 旧页面连接、blocked、迁移事务和失败回退的静态关系说明图
图2:升级风险边界结构说明图,查看旧连接、迁移事务与失败回退的关系;这是说明图,不是运行截图。
// 让旧页面在收到版本变更通知后关闭连接,避免新版本长期 blocked。
function openNotesDatabase(version) {
  const request = indexedDB.open("notes-db", version);

  request.onblocked = () => {
    // blocked 说明仍有旧连接占用版本变更锁,应提示用户关闭旧页面。
    console.warn("Please close an older tab before upgrading the database");
  };

  request.onsuccess = (event) => {
    const db = event.target.result;
    db.onversionchange = () => {
      // 旧连接主动关闭,避免阻塞其他页面的升级请求。
      db.close();
    };
    db.onerror = () => {
      // 连接级错误只做记录,具体事务错误仍由事务自身处理。
      console.error("IndexedDB connection error", db.error);
    };
  };

  request.onerror = () => {
    // open 失败时不要替换本地数据,交给上层决定重试或降级。
    console.error("IndexedDB open failed", request.error);
  };

  return request;
}

升级失败时不要把数据库版本写入应用自己的配置表来“标记成功”。浏览器会根据升级事务是否完成来维护版本;迁移请求报错或事务被 abort 时,应用应停止使用未完成的目标结构,并根据错误类型提示重试、关闭旧页面或清理明确可重建的缓存。

迁移时最容易忽略的四个边界

边界应该怎么做不要怎么做
版本跳跃用 oldVersion 依次补齐每个版本只处理恰好从 n-1 升级的情况
名称重复创建前检查对象仓库或索引名称每次打开都无条件 create
主键变化临时仓库搬运数据后再替换旧仓库直接删除旧仓库再期待数据恢复
旧连接阻塞监听 versionchange、blocked,及时关闭旧连接只监听 onsuccess,忽略 blocked

可以把一次迁移的发布检查压缩成三问:目标版本是否递增?每个旧版本是否都有明确路径?迁移失败时旧数据是否仍可回退?这三问比单纯检查“打开请求成功”更能发现结构升级风险。

常见问题

新增索引也需要提升数据库版本吗?

需要。索引属于对象仓库结构,应该通过更高版本触发 upgradeneeded,在版本变更事务中创建或删除。

能不能在普通 readwrite 事务中创建对象仓库?

不能。对象仓库和索引的结构调整属于版本变更事务,普通读写事务只适合读写已有结构中的记录。

迁移时直接 deleteObjectStore 会怎样?

仓库中的记录会被删除。如果旧数据仍有价值,应先在同一个升级事务中读取并写入新仓库,确认搬运完成后再删除旧仓库。

为什么新版本一直没有触发升级?

先确认传给 indexedDB.open() 的版本号确实高于当前数据库版本;如果版本没有增加,浏览器不会把普通打开当作结构升级。

小结

IndexedDB 迁移的核心不是在启动时反复“修库”,而是把每次结构变化写成递增版本。新增仓库和索引放进 onupgradeneeded,主键路径变化采用临时仓库搬运,旧页面通过 versionchange 关闭连接,并为 blocked、error 和 abort 留下清晰的失败处理。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>