跳到主要内容

使用数据库

数据库的选择

根据实际情况,有很多数据库可以选择, 但我们强烈推荐你使用 支持 JSON 嵌套结构 的 NoSQL 数据库,因为:

  1. JSON 嵌套结构更利于发挥 TypeScript 类型特性,参考 类型设计
  2. 大幅简化关系型表结构设计,降低维护成本
  3. 无需学习 SQL,纯 API 式调用更容易上手,也规避了 SQL 注入等安全风险

例如 MongoDB 就是一个非常不错的选择。

使用 MongoDB

MongoDB 是成熟的 NoSQL 数据库,对 TypeScript 支持十分良好。

note

MongoDB Atlas 是官方推出的云数据库服务,提供 512 MB 免费空间,非常适合学习使用。

安装

MongoDB 已经提供了官方的 NodeJS 驱动:

npm i mongodb
# 或者
yarn add mongodb

然后就可以在代码中使用了,使用细节可以参阅官方文档

配置和启动

MongoDB 的客户端都是异步的 API,并且会自动维护连接池,所以你只需要全局创建一个共享的 Db 实例即可, 例如:

import { Db, MongoClient } from "mongodb";

export class Global {
static db: Db;

static async initDb() {
const uri = 'mongodb://username:password@xxx.com:27017/test?authSource=admin';
const client = await new MongoClient(uri).connect();
this.db = client.db();
}
}
tip

安全起见,你可以将连接配置放在配置文件或环境变量中。

你需要在服务启动前就连接好数据库,所以修改一下 index.ts 中的初始化流程:

index.ts
// Initialize
async function init() {
// Auto implement APIs
await server.autoImplementApi(path.resolve(__dirname, 'api'));
// 在服务启动前先连接好数据库
await Global.initDb();
};

// Entry function
async function main() {
await init();
await server.start();
};
main();

然后,就可以使用 Global.db 来调用 MongoDB 了,例如:

export async function ApiGetPost(call: ApiCall<ReqGetPost, ResGetPost>) {
let op = await Global.db.collection<Post>('Post').findOne({
_id: call.req._id
});
// ...
}
tip

通常你不需要手动去关闭连接,保持数据库的长连接可以让你的接口响应更加迅速。

表结构映射

在上面的例子中看到,我们可以通过 db.collection<类型名>('表名') 这样的写法来告诉 TypeScript 表结构类型。 但是,你又给自己埋下了一个小坑。

墨菲定律:可能犯错的一定会犯错。

如果表名拼写错了呢?如果类型名关联错了呢?这些都是常有的事。

同样,你也可以利用 TypeScript 的类型系统,在一开始就规避这些问题。 首先,定义一个 interface,显示指明所有表名及其类型:

export interface DbCollectionType {
// 表名:类型名
Post: DbPost,
User: DbUser,
Comment: DbComment
}

然后,自行实现一个 .collection 方法,利用 TS 的泛型,自动关联表名和类型名:

import { Collection, Db, MongoClient, OptionalId } from "mongodb";

export class Global {

static db: Db;
static async initDb() { ... }

static collection<T extends keyof DbCollectionType>(col: T): Collection<OptionalId<DbCollectionType[T]>> {
return this.db.collection(col);
}
}

现在,你就可以使用 Global.collection 来替代 Global.db.collection 了,享有了自动代码提示和类型约束。

ObjectId 和 Date

ObjectId

在 TSRPC 下,你可以在协议中直接使用 ObjectId 类型,框架会自动完成传输前后的类型转换。

ObjectId 是 MongoDB 默认的 _id 类型,因为其引用自 mongodb NPM 包(前端未安装),所以通常无法在前端通用。 但通过 脚手架工具 创建的 TSRPC 全栈项目却做到了这一点,其原理是在前端项目中的 env.d.ts 中定义了如下类型:

declare module 'mongodb' {
export type ObjectId = string;
export type ObjectID = string;
}
declare module 'bson' {
export type ObjectId = string;
export type ObjectID = string;
}

因此即便协议定义中包含了 import { ObjectId } from 'mongodb',亦可以在前端通用。ObjectId 在前端被解析为普通字符串,但在后端将自动转换为 ObjectId 类型。

Date

在 TSRPC 下,你可以在协议中直接使用 Date 类型。

推荐直接使用 Date 类型而不是时间戳,通常它在数据库管理工具中可读性更佳,看数据、维护都更轻松。

例子:增删改查

https://github.com/k8w/tsrpc-examples/tree/main/examples/mongodb-crud

使用 MySQL

danger

TODO

减少类型冗余

CRUD 接口常见的场景是,对于数据表结构,只允许客户端发送有限的字段,其余字段由客户端来维护。 利用 TypeScript 工具类型 PickOmitPartial,你也可以在最小冗余的情况下定义它们,例如:

PtlAddArticle.ts
import { ObjectId } from 'mongodb';
import { Article } from './Article';

// 新建文章
export interface ReqAddArticle {
// 不需要填写 `_id` 和服务端维护的字段,用 Omit 剔除之
article: Omit<Article, '_id' | 'create' | 'update'>;
}

export interface ResAddArticle {
_id: ObjectId
}
tip

即便客户端发送了协议以外的额外字段,TSRPC 类型系统也会自动剔除它们,确保类型和字段的严格安全。