目录

第五部分: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() 的注意事项

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 操作:

继续阅读 第六部分:事务(Transaction)详解