跳到主要内容

TypeScript 中的数据建模

Deno KV 目前处于测试阶段

Deno 的 KV 及其相关的云基元 API,如队列和定时任务,目前仍处于实验性阶段,可能会发生变化。尽管我们尽力确保数据持久性,但特别是在 Deno 更新时,仍然存在数据丢失的可能性。

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

deno run -A --unstable my_kv_code.ts

在 TypeScript 应用程序中,通常希望创建强类型且有文档记录的对象来包含应用程序操作的数据。使用 interfacesclasses,您可以描述程序中对象的形状和行为。

然而,如果您正在使用 Deno KV,那么需要额外的工作来持久化和检索强类型对象。在本指南中,我们将介绍如何处理与 Deno KV 的强类型对象的工作策略。

使用 interfaces 和类型断言

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

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 中,您可以像使用 data transfer objects (DTOs) 一样使用这些 TypeScript interfaces,它们是对您可能发送到 Deno KV 或从 Deno KV 接收的无类型对象的强类型包装器。

在没有任何额外的工作的情况下,您可以愉快地将这些 DTOs 的内容存储在 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 检索相同的对象时,默认情况下不会附带类型信息。但是,如果您知道存储在键下的对象的形状,您可以使用 type assertion 来通知 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 指定一个可选的 type parameter:

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

const kv = await Deno.openKv();

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

console.log(r.value.fullName);

对于更简单的数据结构,这种技巧可能已经足够。但通常,当创建或访问您的领域对象时,您将希望或需要应用一些业务逻辑。当出现这种需求时,您可以开发一组纯函数,这些函数可以操作您的 DTOs。

使用服务层封装业务逻辑

当您的应用程序的持久性需求变得更复杂,比如当您需要创建 secondary indexes 以按不同键查询数据,或者维护对象之间的关系时,您将希望创建一组函数,位于 DTOs 之上,以确保传递的数据是有效的(而不仅仅是正确地类型化)。

从上面的业务对象中,Post 对象足够复杂,可能需要一小层代码来保存和检索对象的实例。以下是两个函数的示例,它们包装了底层的 Deno KV APIs,并为 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 操作,以便可以正确地序列化和 "hydrate" 模型对象,使其具有适当的类型和关联。