====== 第五部分:CRUD 操作详解 ======
===== 5.1 CRUD 概述 =====
CRUD 是数据库操作的四大基本功能:创建(Create)、读取(Read)、更新(Update)、删除(Delete)。在 IndexedDB 中,这些操作对应 add、get/put、delete 等方法。本章将深入讲解每种操作的使用方式、注意事项和最佳实践。
===== 5.2 创建操作(Create) =====
===== 5.2.1 add() 方法详解 =====
add() 方法用于向对象存储空间插入新记录。如果主键已存在,操作会失败。
// 基本语法
const request = objectStore.add(value);
const request = objectStore.add(value, key); // 外联键时使用
// 完整示例
function addUser(user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = function(event) {
// event.target.result 是新增记录的主键
resolve(event.target.result);
};
request.onerror = function(event) {
reject(event.target.error);
};
});
}
// 使用示例
addUser({
id: 'user_001',
username: 'zhangsan',
email: 'zhangsan@example.com',
createdAt: new Date().toISOString()
}).then(key => {
console.log('用户创建成功,主键:', key);
}).catch(error => {
if (error.name === 'ConstraintError') {
console.error('主键已存在,请使用 put() 更新');
} else {
console.error('创建失败:', error);
}
});
===== 5.2.2 批量添加 =====
当需要添加多条记录时,应该在同一个事务中执行:
function addMultipleUsers(users) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const results = [];
let completed = 0;
users.forEach(user => {
const request = store.add(user);
request.onsuccess = () => {
results.push({ status: 'success', key: request.result });
completed++;
if (completed === users.length) {
resolve(results);
}
};
request.onerror = () => {
results.push({ status: 'error', error: request.error, user });
completed++;
if (completed === users.length) {
resolve(results); // 部分成功也 resolve
}
};
});
transaction.onerror = () => reject(transaction.error);
});
}
// 批量添加 1000 条测试数据
async function seedDatabase() {
const users = [];
for (let i = 1; i <= 1000; i++) {
users.push({
id: `user_${String(i).padStart(6, '0')}`,
username: `user${i}`,
email: `user${i}@example.com`,
age: Math.floor(Math.random() * 50) + 18,
department: ['技术', '产品', '设计', '运营'][Math.floor(Math.random() * 4)],
createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 3600 * 1000).toISOString()
});
}
const startTime = performance.now();
const results = await addMultipleUsers(users);
const duration = performance.now() - startTime;
const successCount = results.filter(r => r.status === 'success').length;
console.log(`批量添加完成: ${successCount}/${users.length} 成功, 耗时: ${duration.toFixed(2)}ms`);
}
===== 5.2.3 add() 的注意事项 =====
* **主键冲突**:如果主键已存在,会触发 ConstraintError,事务不会回滚(除非显式设置)
* **数据克隆**:add() 使用结构化克隆算法,函数、DOM 节点等无法存储
* **键路径**:如果使用 keyPath,对象中必须包含该属性(除非使用 autoIncrement)
* **事务自动提交**:事务在所有请求完成后自动提交,无需手动调用
===== 5.3 读取操作(Read) =====
===== 5.3.1 get() 方法详解 =====
get() 通过主键获取单条记录:
// 基本语法
const request = objectStore.get(key);
// 示例
function getUserById(userId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(userId);
request.onsuccess = () => {
if (request.result) {
resolve(request.result);
} else {
resolve(null); // 记录不存在
}
};
request.onerror = () => reject(request.error);
});
}
// 使用
const user = await getUserById('user_001');
if (user) {
console.log('用户信息:', user);
} else {
console.log('用户不存在');
}
===== 5.3.2 getAll() 方法详解 =====
getAll() 获取所有或指定范围内的记录:
// 获取所有记录
const request1 = store.getAll();
// 获取指定主键的记录(配合索引使用)
const index = store.index('ageIndex');
const request2 = index.getAll(25); // 获取所有 25 岁的用户
// 获取范围内的记录
const range = IDBKeyRange.bound(20, 30);
const request3 = index.getAll(range); // 获取年龄在 20-30 之间的用户
// 限制返回数量
const request4 = index.getAll(null, 10); // 最多返回 10 条
// 完整示例:分页查询
async function getUsersByPage(page = 1, pageSize = 20) {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const allUsers = await store.getAll();
const start = (page - 1) * pageSize;
const end = start + pageSize;
return {
data: allUsers.slice(start, end),
total: allUsers.length,
page,
pageSize,
totalPages: Math.ceil(allUsers.length / pageSize)
};
}
===== 5.3.3 通过索引查询 =====
// 使用索引查询
function findUsersByDepartment(department) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('departmentIndex');
const request = index.getAll(department);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用复合索引查询
function findUsersByAgeRange(minAge, maxAge) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('ageIndex');
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
===== 5.4 更新操作(Update) =====
===== 5.4.1 put() 方法详解 =====
put() 用于更新现有记录或插入新记录:
// 基本语法
const request = objectStore.put(value);
const request = objectStore.put(value, key); // 外联键时使用
// 更新示例
async function updateUser(userId, updates) {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// 先获取原记录
const user = await new Promise((resolve, reject) => {
const request = store.get(userId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!user) {
throw new Error('用户不存在');
}
// 合并更新
const updatedUser = { ...user, ...updates, updatedAt: new Date().toISOString() };
// 保存更新
return new Promise((resolve, reject) => {
const request = store.put(updatedUser);
request.onsuccess = () => resolve(updatedUser);
request.onerror = () => reject(request.error);
});
}
// 使用
await updateUser('user_001', {
email: 'newemail@example.com',
department: '产品'
});
===== 5.4.2 部分更新策略 =====
// 策略一:全量替换(简单但可能丢失数据)
store.put({ id: 'user_001', name: '张三', email: 'new@example.com' });
// 策略二:先读后写(推荐)
async function partialUpdate(storeName, key, updates) {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const existing = await store.get(key);
if (!existing) throw new Error('记录不存在');
const updated = { ...existing, ...updates };
await store.put(updated);
return updated;
}
// 策略三:使用补丁对象
function applyPatch(original, patch) {
const result = { ...original };
for (const [key, value] of Object.entries(patch)) {
if (value === undefined) {
delete result[key]; // undefined 表示删除
} else {
result[key] = value;
}
}
return result;
}
===== 5.5 删除操作(Delete) =====
===== 5.5.1 delete() 方法详解 =====
// 删除单条记录
const request = store.delete(key);
// 删除范围内的记录
const range = IDBKeyRange.bound('A', 'M');
const request2 = store.delete(range);
// 完整示例
function deleteUser(userId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// 先检查记录是否存在
const getRequest = store.get(userId);
getRequest.onsuccess = () => {
if (!getRequest.result) {
resolve({ success: false, message: '记录不存在' });
return;
}
const deleteRequest = store.delete(userId);
deleteRequest.onsuccess = () => {
resolve({ success: true, deleted: getRequest.result });
};
deleteRequest.onerror = () => reject(deleteRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
===== 5.5.2 clear() 清空 =====
// 清空整个对象存储空间(谨慎使用!)
function clearStore(storeName) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => {
console.log(`${storeName} 已清空`);
resolve();
};
request.onerror = () => reject(request.error);
});
}
===== 5.6 高级查询技巧 =====
===== 5.6.1 多条件查询 =====
IndexedDB 不直接支持多条件 AND/OR 查询,需要通过代码实现:
// AND 查询:年龄 > 25 且部门 = '技术'
async function findSeniorDevelopers() {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const ageIndex = store.index('ageIndex');
// 先通过索引过滤年龄
const range = IDBKeyRange.lowerBound(26);
const candidates = await ageIndex.getAll(range);
// 再在内存中过滤部门
return candidates.filter(user => user.department === '技术');
}
// OR 查询:部门 = '技术' 或 部门 = '产品'
async function findTechAndProduct() {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const deptIndex = store.index('departmentIndex');
const techUsers = await deptIndex.getAll('技术');
const productUsers = await deptIndex.getAll('产品');
// 合并并去重
const combined = [...techUsers, ...productUsers];
const seen = new Set();
return combined.filter(user => {
if (seen.has(user.id)) return false;
seen.add(user.id);
return true;
});
}
===== 5.6.2 模糊查询 =====
IndexedDB 不支持 LIKE 查询,但可以通过范围查询模拟:
// 前缀匹配(模拟 LIKE '张%')
function findUsersByPrefix(prefix) {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('nameIndex');
// 创建前缀范围
const range = IDBKeyRange.bound(prefix, prefix + '\uffff');
return index.getAll(range);
}
// 使用示例
const zhangUsers = await findUsersByPrefix('张');
// 返回 '张三', '张三丰', '张无忌' 等
===== 5.6.3 排序与分页 =====
// 按索引排序并分页
async function getUsersSortedAndPaged(options = {}) {
const {
sortBy = 'createdAt',
sortOrder = 'desc',
page = 1,
pageSize = 20
} = options;
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
// 使用索引排序
let index;
try {
index = store.index(`${sortBy}Index`);
} catch (e) {
// 如果没有对应索引,使用主键
index = store;
}
const direction = sortOrder === 'desc' ? 'prev' : 'next';
return new Promise((resolve, reject) => {
const results = [];
let skipped = 0;
const skip = (page - 1) * pageSize;
const request = index.openCursor(null, direction);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return;
}
if (skipped < skip) {
skipped++;
cursor.continue();
return;
}
if (results.length < pageSize) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
===== 5.7 本章小结 =====
本章详细介绍了 IndexedDB 的 CRUD 操作:
* add() 插入新记录,主键冲突会失败
* get() / getAll() 读取记录,支持通过索引查询
* put() 更新或插入记录
* delete() / clear() 删除记录或清空存储
* 多条件查询、模糊查询需要通过代码组合实现
* 分页查询结合游标和索引实现
继续阅读 [[part06-transaction|第六部分:事务(Transaction)详解]]。