跳至主要内容

TypeScript 中的数据建模

Deno KV 目前处于测试阶段

Deno KV 和相关的云原生 API(如队列和 cron)目前处于实验阶段,并且可能会发生变化。虽然我们尽力确保数据持久性,但数据丢失是可能的,尤其是在 Deno 更新期间。

使用 KV 的 Deno 程序在启动程序时需要 --unstable 标志,如下所示

deno run -A --unstable my_kv_code.ts

在 TypeScript 应用程序中,通常希望创建强类型、文档完善的对象来包含应用程序操作的数据。使用 接口,可以描述程序中对象的形状和行为。

但是,如果您使用的是 Deno KV,则需要做一些额外的工作才能持久化和检索强类型对象。在本指南中,我们将介绍在 Deno KV 中进出强类型对象的工作策略。

使用接口和类型断言

在 Deno KV 中存储和检索应用程序数据时,您可能希望首先使用 TypeScript 接口描述数据的形状。以下是描述博客系统一些关键组件的对象模型。

model.ts
export interface Author {
username: string;
fullName: string;
}

export interface Post {
slug: string;
title: string;
body: string;
author: Author;
createdAt: Date;
updatedAt: Date;
}

此对象模型描述了博客文章和关联的作者。

使用 Deno KV,您可以像 数据传输对象 (DTO) 一样使用这些 TypeScript 接口 - 围绕您可能发送到或接收自 Deno KV 的无类型对象进行强类型包装。

无需任何额外工作,您就可以愉快地将其中一个 DTO 的内容存储在 Deno KV 中。

import { Author } from "./model.ts";

const kv = await Deno.openKv();

const a: Author = {
username: "acdoyle",
fullName: "Arthur Conan Doyle",
};

await kv.set(["authors", a.username], a);

但是,从 Deno KV 检索同一个对象时,默认情况下它不会与类型信息相关联。但是,如果您知道为键存储的对象的形状,则可以使用 类型断言 来告知 TypeScript 编译器对象的形状。

import { Author } from "./model.ts";

const kv = await Deno.openKv();

const r = await kv.get(["authors", "acdoyle"]);
const ac = r.value as Author;

console.log(ac.fullName);

您还可以为 get 指定一个可选的 类型参数

import { Author } from "./model.ts";

const kv = await Deno.openKv();

const r = await kv.get<Author>(["authors", "acdoyle"]);

console.log(r.value.fullName);

对于更简单的數據結構,这种技术可能就足够了。但通常,您会希望或需要在创建或访问域对象时应用一些业务逻辑。当出现这种需求时,您可以开发一组可以在 DTO 上操作的纯函数。

使用服务层封装业务逻辑

当您的应用程序的持久性需求变得更加复杂时 - 例如,当您需要创建 二级索引 以通过不同的键查询您的数据,或维护对象之间的关系时 - 您将希望创建一组函数来位于您的 DTO 之上,以确保传递的数据是有效的(而不仅仅是类型正确)。

从上面的业务对象来看,Post 对象足够复杂,可能需要一层代码来保存和检索对象的实例。以下是一个示例,其中包含两个函数,它们包装了底层的 Deno KV API,并为 Post 接口返回强类型对象实例。

值得注意的是,我们需要存储 Author 对象的标识符,以便我们以后可以从 KV 中检索作者信息。

import { Author, Post } from "./model.ts";

const kv = await Deno.openKv();

interface RawPost extends Post {
authorUsername: string;
}

export async function savePost(p: Post): Promise<Post> {
const postData: RawPost = Object.assign({}, p, {
authorUsername: p.author.username,
});

await kv.set(["posts", p.slug], postData);
return p;
}

export async function getPost(slug: string): Promise<Post> {
const postResponse = await kv.get(["posts", slug]);
const rawPost = postResponse.value as RawPost;
const authorResponse = await kv.get(["authors", rawPost.authorUsername]);

const author = authorResponse.value as Author;
const post = Object.assign({}, postResponse.value, {
author,
}) as Post;

return post;
}

这层薄薄的代码使用了一个名为 RawPost 的接口,它扩展了实际的 Post 接口,并包含了一些额外的用于引用其他索引数据(关联的 Author 对象)的数据。

savePostgetPost 函数取代了直接的 Deno KV getset 操作,以便它们可以为我们正确地序列化和“水化”模型对象,并使用适当的类型和关联。