跳到主要内容

使用数据库

数据库的选择#

根据实际情况,有很多数据库可以选择, 但我们强烈推荐你使用 支持 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 全栈项目却做到了这一点,其原理是在前端项目中的 end.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 类型系统也会自动剔除它们,确保类型和字段的严格安全。