第三部分:对象存储空间(Object Store)详解
3.1 对象存储空间概述
对象存储空间(Object Store)是 IndexedDB 中最核心的概念,它是实际存储数据的地方。理解对象存储空间的设计和使用方式,是掌握 IndexedDB 的关键。
3.1.1 什么是对象存储空间
对象存储空间可以类比为关系型数据库中的“表”,但有几个重要区别:
- 无模式(Schema-less):同一对象存储空间中的对象不需要有相同的结构
- 对象导向:存储的是 JavaScript 对象,而非行记录
- 主键必需:每个对象必须有一个唯一的主键
- 索引可选:可以创建索引加速查询,但不是必须的
3.1.2 对象存储空间的特性
| 特性 | 说明 |
|---|---|
| 存储类型 | 结构化克隆算法支持的任何 JavaScript 对象 |
| 主键要求 | 每个对象必须有唯一主键 |
| 数据类型 | 对象、数组、日期、Blob、ArrayBuffer 等 |
| 容量限制 | 受浏览器存储配额限制 |
| 并发访问 | 通过事务控制,同一时间只有一个写入事务 |
3.2 创建对象存储空间
3.2.1 createObjectStore 方法
// 基本语法 const store = db.createObjectStore(storeName, options); // options 参数 { keyPath: string | string[], // 指定主键属性路径 autoIncrement: boolean // 是否自动生成主键 }
3.2.2 主键设计策略
策略一:使用内联键(keyPath)
// 单一属性作为主键 db.createObjectStore('users', { keyPath: 'userId' }); // 使用对象时 store.add({ userId: 'U001', name: '张三', email: 'zhangsan@example.com' });
优点:
- 主键与数据紧密关联
- 查询直观,知道主键就知道数据位置
缺点:
- 需要应用层生成唯一 ID
- 不能更改主键值
策略二:使用自增键(autoIncrement)
// 自动生成递增整数 db.createObjectStore('logs', { autoIncrement: true }); // 插入数据时不需要指定主键 store.add({ message: '用户登录', timestamp: new Date(), level: 'info' }); // 自动分配主键 1, 2, 3...
优点:
- 无需应用层管理主键
- 保证唯一性和顺序性
缺点:
- 主键是数字,不携带业务信息
- 删除数据后主键不会复用
策略三:组合使用 keyPath 和 autoIncrement
// 自动生成主键并存储在对象的 id 属性中 db.createObjectStore('items', { keyPath: 'id', autoIncrement: true }); // 插入后对象会自动包含 id 属性 const request = store.add({ name: '商品A', price: 100 }); request.onsuccess = () => { console.log('分配的主键:', request.result); // 1 };
策略四:复合主键
// 使用多个属性组成复合主键 db.createObjectStore('memberships', { keyPath: ['userId', 'projectId'] }); // 插入数据 store.add({ userId: 'U001', projectId: 'P100', role: 'admin', joinedAt: new Date() }); // 查询时使用数组作为键 store.get(['U001', 'P100']);
3.3 对象存储空间配置详解
3.3.1 创建时的索引配置
request.onupgradeneeded = function(event) { const db = event.target.result; const store = db.createObjectStore('products', { keyPath: 'sku' }); // 普通索引 store.createIndex('categoryIndex', 'category', { unique: false }); // 唯一索引 store.createIndex('barcodeIndex', 'barcode', { unique: true }); // 多入口索引(用于数组属性) store.createIndex('tagIndex', 'tags', { multiEntry: true }); // 复合索引 store.createIndex('priceCategoryIndex', ['price', 'category']); };
3.3.2 对象存储空间属性
const store = transaction.objectStore('users'); // 获取对象存储空间名称 console.log(store.name); // 'users' // 获取主键路径 console.log(store.keyPath); // 'id' 或 null // 检查是否使用自增主键 console.log(store.autoIncrement); // true 或 false // 获取索引列表 console.log(store.indexNames); // DOMStringList ['nameIndex', 'emailIndex']
3.4 数据操作基础
3.4.1 添加数据(add)
const request = store.add(value, key); // 示例 const addRequest = store.add({ id: 'user001', name: '张三', email: 'zhangsan@example.com', profile: { age: 25, city: '北京' } }); addRequest.onsuccess = function() { console.log('添加成功,主键:', addRequest.result); }; addRequest.onerror = function() { // 如果主键已存在,会触发错误 console.error('添加失败:', addRequest.error); };
注意事项:
- 如果主键已存在,add() 会失败(触发 ConstraintError)
- 如果不指定 key 参数,使用 keyPath 或 autoIncrement
- 数据通过结构化克隆算法存储,函数和某些 DOM 对象不能被存储
3.4.2 更新/插入数据(put)
// put 会覆盖已存在的记录,或插入新记录 const request = store.put(value, key); // 更新现有记录 store.put({ id: 'user001', // 相同主键 name: '张三(已更新)', email: 'newemail@example.com' }); // 插入新记录(如果不存在) store.put({ id: 'user002', name: '李四', email: 'lisi@example.com' });
add() vs put() 对比:
| 方法 | 主键已存在 | 主键不存在 |
|---|---|---|
| add() | 错误 (ConstraintError) | 插入新记录 |
| put() | 覆盖现有记录 | 插入新记录 |
3.4.3 删除数据(delete)
// 删除指定主键的记录 const deleteRequest = store.delete(key); // 示例 store.delete('user001'); // 也可以使用 IDBKeyRange 删除范围 store.delete(IDBKeyRange.bound('A', 'M'));
3.4.4 清空对象存储空间(clear)
// 删除所有记录 const clearRequest = store.clear(); clearRequest.onsuccess = function() { console.log('对象存储空间已清空'); };
3.4.5 获取数据(get)
// 通过主键获取 const getRequest = store.get(key); getRequest.onsuccess = function() { if (getRequest.result) { console.log('找到数据:', getRequest.result); } else { console.log('数据不存在'); } }; // 获取所有记录(配合游标,见第七部分) // 获取记录数量 const countRequest = store.count(); countRequest.onsuccess = () => { console.log('记录数:', countRequest.result); };
3.5 数据类型支持
3.5.1 结构化克隆算法
IndexedDB 使用结构化克隆算法序列化数据,支持以下类型:
- 基本类型:Boolean, Number, String, null, undefined
- 日期:Date 对象
- 正则表达式:RegExp(但不支持标志)
- 二进制数据:Blob, File, FileList, ArrayBuffer, TypedArray, DataView
- 图像数据:ImageData
- 集合:Array, Object, Map, Set
- 特殊对象:Error(部分属性可能丢失)
3.5.2 不支持的类型
以下类型不能直接存储:
- Function:函数对象
- DOM 对象:大多数 DOM 节点
- 某些内置对象:如 WeakMap, WeakSet
- 循环引用:对象间存在循环引用时会抛出 DataCloneError
3.5.3 处理不支持的数据
// 方案一:将函数转换为字符串 const obj = { name: 'test', // 存储前转换 fn: myFunction.toString() }; // 方案二:使用 JSON 序列化(丢失类型信息) const serialized = JSON.stringify(obj); // 存储字符串 store.add({ id: 1, data: serialized }); // 方案三:自定义序列化 function serialize(obj) { return JSON.stringify(obj, (key, value) => { if (value instanceof Date) { return { __type: 'Date', value: value.toISOString() }; } return value; }); } function deserialize(str) { return JSON.parse(str, (key, value) => { if (value && value.__type === 'Date') { return new Date(value.value); } return value; }); }
3.6 对象存储空间设计模式
3.6.1 单类型存储模式
每个对象存储空间只存储一种类型的数据:
// 用户存储 const userStore = db.createObjectStore('users', { keyPath: 'id' }); // 订单存储 const orderStore = db.createObjectStore('orders', { keyPath: 'orderId' }); // 产品存储 const productStore = db.createObjectStore('products', { keyPath: 'sku' });
优点:
- 结构清晰,易于维护
- 索引针对特定类型优化
- 事务粒度合理
3.6.2 多态存储模式
在同一个对象存储空间中存储不同类型,通过 type 字段区分:
const store = db.createObjectStore('items', { keyPath: 'id' }); store.createIndex('typeIndex', 'type', { unique: false }); // 存储用户 store.add({ id: 'u1', type: 'user', name: '张三' }); // 存储产品 store.add({ id: 'p1', type: 'product', title: 'iPhone' }); // 存储文章 store.add({ id: 'a1', type: 'article', title: 'IndexedDB 教程' });
优点:
- 可以统一查询所有类型
- 适合日志等类型相似的数据
缺点:
- 索引设计复杂
- 类型检查需要在应用层处理
3.6.3 关系存储模式
模拟关系型数据库的外键关系:
// 用户表 const userStore = db.createObjectStore('users', { keyPath: 'userId' }); // 订单表,包含 userId 作为外键 const orderStore = db.createObjectStore('orders', { keyPath: 'orderId' }); orderStore.createIndex('userIdIndex', 'userId', { unique: false }); // 订单项表 const itemStore = db.createObjectStore('orderItems', { keyPath: 'itemId' }); itemStore.createIndex('orderIdIndex', 'orderId', { unique: false });
3.7 本章小结
本章深入介绍了对象存储空间:
- 对象存储空间是 IndexedDB 的核心数据容器
- 主键设计有内联键、自增键、复合键等多种策略
- 基本操作包括 add、put、delete、clear、get
- 使用结构化克隆算法存储数据,注意不支持函数等类型
- 设计模式包括单类型、多态和关系存储
继续阅读 第四部分:索引(Index)详解。