在本页

二级索引

像 Deno KV 这样的键值存储将数据组织为键值对的集合,其中每个唯一键都与单个值相关联。这种结构可以根据键轻松检索值,但不能根据值本身进行查询。为了克服这种限制,您可以创建二级索引,这些索引在包含(部分)该值的附加键下存储相同的值。

使用二级索引时,维护主索引和二级索引之间的一致性至关重要。如果在不更新二级索引的情况下更新主索引处的某个值,则从针对二级索引的查询返回的数据将不正确。为了确保主索引和二级索引始终代表相同的数据,请在插入、更新或删除数据时使用原子操作。这种方法确保一组变异操作作为单个单元执行,并且要么全部成功,要么全部失败,从而防止不一致。

唯一索引(一对一) 跳转到标题

唯一索引的每个索引键都与一个主索引键相关联。例如,当存储用户数据并通过其唯一 ID 和电子邮件地址查找用户时,请在两个单独的键下存储用户数据:一个用于主索引(用户 ID),另一个用于二级索引(电子邮件)。这种设置允许根据 ID 或电子邮件查询用户。二级索引还可以对存储中的值强制执行唯一性约束。在用户数据的情况下,使用索引确保每个电子邮件地址仅与一个用户相关联 - 换句话说,电子邮件是唯一的。

要为此示例实现唯一的二级索引,请执行以下步骤

  1. 创建一个表示数据的 User 接口

    interface User {
      id: string;
      name: string;
      email: string;
    }
    
  2. 定义一个 insertUser 函数,该函数在主索引和二级索引处存储用户数据

    async function insertUser(user: User) {
      const primaryKey = ["users", user.id];
      const byEmailKey = ["users_by_email", user.email];
      const res = await kv.atomic()
        .check({ key: primaryKey, versionstamp: null })
        .check({ key: byEmailKey, versionstamp: null })
        .set(primaryKey, user)
        .set(byEmailKey, user)
        .commit();
      if (!res.ok) {
        throw new TypeError("User with ID or email already exists");
      }
    }
    

    此函数使用原子操作执行插入,该操作检查是否存在具有相同 ID 或电子邮件的用户。如果这些约束中的任何一个被违反,则插入失败,并且不会修改任何数据。

  3. 定义一个getUser函数,通过用户 ID 获取用户。

    async function getUser(id: string): Promise<User | null> {
      const res = await kv.get<User>(["users", id]);
      return res.value;
    }
    
  4. 定义一个getUserByEmail函数,通过用户电子邮件地址获取用户。

    async function getUserByEmail(email: string): Promise<User | null> {
      const res = await kv.get<User>(["users_by_email", email]);
      return res.value;
    }
    

    此函数使用辅助键 (["users_by_email", email]) 查询存储。

  5. 定义一个deleteUser函数,通过用户 ID 删除用户。

    async function deleteUser(id: string) {
      let res = { ok: false };
      while (!res.ok) {
        const getRes = await kv.get<User>(["users", id]);
        if (getRes.value === null) return;
        res = await kv.atomic()
          .check(getRes)
          .delete(["users", id])
          .delete(["users_by_email", getRes.value.email])
          .commit();
      }
    }
    

    此函数首先通过用户 ID 获取用户,以获取用户的电子邮件地址。这需要检索用于构建此用户地址的辅助索引的键所需的电子邮件。然后,它执行一个原子操作,检查数据库中的用户是否已更改,然后删除指向用户值的初级和辅助键。如果失败(用户在查询和删除之间已修改),原子操作将中止。整个过程将被重试,直到删除成功。此检查是必需的,以防止在检索和删除之间可能修改值的竞争条件。如果更新更改了用户的电子邮件,则可能会发生这种竞争,因为在这种情况下辅助索引会移动。然后,辅助索引的删除将失败,因为删除针对的是旧的辅助索引键。

非唯一索引(一对多) 跳转到标题

非唯一索引是辅助索引,其中单个键可以与多个主鍵关联,允许您根据共享属性查询多个项目。例如,当通过用户喜欢的颜色查询用户时,使用非唯一辅助索引实现这一点。喜欢的颜色是非唯一属性,因为多个用户可以有相同的喜欢的颜色。

要实现此示例的非唯一辅助索引,请按照以下步骤操作

  1. 定义User接口

    interface User {
      id: string;
      name: string;
      favoriteColor: string;
    }
    
  2. 定义insertUser函数

    async function insertUser(user: User) {
      const primaryKey = ["users", user.id];
      const byColorKey = [
        "users_by_favorite_color",
        user.favoriteColor,
        user.id,
      ];
      await kv.atomic()
        .check({ key: primaryKey, versionstamp: null })
        .set(primaryKey, user)
        .set(byColorKey, user)
        .commit();
    }
    
  3. 定义一个函数,通过用户喜欢的颜色检索用户。

    async function getUsersByFavoriteColor(color: string): Promise<User[]> {
      const iter = kv.list<User>({ prefix: ["users_by_favorite_color", color] });
      const users = [];
      for await (const { value } of iter) {
        users.push(value);
      }
      return users;
    }
    

此示例演示了非唯一辅助索引users_by_favorite_color的使用,该索引允许根据用户喜欢的颜色查询用户。主鍵仍然是用户id

唯一索引和非唯一索引实现之间的主要区别在于辅助键的结构和组织。在唯一索引中,每个辅助键都与一个主鍵关联,确保索引属性在所有记录中都是唯一的。在非唯一索引的情况下,单个辅助键可以与多个主鍵关联,因为索引属性可能在多个记录之间共享。为了实现这一点,非唯一辅助键通常以附加的唯一标识符(例如,主鍵)作为键的一部分进行结构化,允许具有相同属性的多个记录共存而不会发生冲突。