Table of Contents
以前写后端都是用 Express 的,但现在流行的是 NestJS,为了跟上潮流,也只能尝试 NestJS 了。对于新的技术栈,碰到问题是难免的,所以这里简要记录碰到的问题。
TypeORM
TypeORM 是一个统一和简化数据库操作的库,比手动拼 SQL 方便一些(熟练以后)。
实体字段如何与数据库不同
很多时候,后端返回给前端的数据名称需要与数据库不同,所以最好有一种映射方法,查询之后自动转换。TypeORM 可以在定义实体(Entity)的时候指定映射字段——Column 装饰器里指定 name。
@Column({ name: 'created_at' }) created: string;
连接多个数据库
网上有很多方法的介绍,比如 app.module 里定义多个 TypeOrmModule.forRoot,以此创建多个连接。我没用这种方式,因为我的库都在一台机器上,这种情况只需要在定义实体的时候指定数据库就行。
@Entity('user', { database: 'lab', })
where 条件嵌套查询
手拼 SQL 的时候我是根据条件拼接不同的 OR 或者 AND 语句,用了 TypeORM 之后方便多了。比如根据条件是否存在调用 where 或者 andWhere 或者 orWhere。
// 关键字查询 if (keyword) { qb.where( '(poem.title LIKE :keyword OR poem.author LIKE :keyword OR poem.content LIKE :keyword)', { keyword: `%${keyword}%` }, ); } // 作者 if (author) { qb.andWhere('poem.author = :author', { author }); } // 类型 if (type) { qb.andWhere('poem.type LIKE :type', { type: `%#${type}#%` }); }
调用很方便,并且即使 keyword 不存在,也不影响后续 andWhere 的调用。而对于嵌套的条件,直接用括号就可以,和手拼一样的(代码里 qb.where 部分)。
还有就是 LIKE 查询的写法。一开始我是这样写的:LIKE `’%#${type}#%’`,这种写法不行,即使把单引号去掉也不行,因为 type 部分会自动替换成包含单引号的实际值。因此,需要直接将值处理成最终的查询条件字符串——{ type: `%#${type}#%` }。
查询结果自动映射实体对象字段
对于创建的查询器,可以调用 execute() 方法执行,但这种方式得到的结果字段是类似于 poem_created_at 这种带有前缀的,并且没有将数据库字段转成我们我需要的字段。并且,如果需要 count,还需要手动调用 getCount() 方法。
其实,为了得到最终结果,只需要调用 getManyAndCount() 方法就可以了,这个方法返回一个数组,数组第一个元素是结果列表,第二个元素是 count 结果。
另外,如果只需要一个结果,直接 getOne() 方法就可以。
分页查询
对于分页,可以调用查询器的 limit() 方法和 offset() 方法。
class-validator
class-validator 是一个配合 NestJS 使用的校验库。
请求参数校验
一开始我以为请求参数直接根据 dto 的 ts 类型就可以校验了,比如:
export class ListReqDto { readonly keyword?: string; readonly page?: number; readonly size?: number; }
但实际上,这样好像并不能触发校验,或者说实际上并没有定义校验条件。字段的具体限制需要添加各种装饰器实现。
export class ListReqDto { @IsString() @IsOptional() readonly keyword?: string; @IsNumber() @IsOptional() readonly page?: number; }
另外,如果需要校验参数为枚举,可以使用 IsIn([xxx, xxx]) 装饰器。
多种类型校验
一个字段可能存在多种类型,比如查询列表,可以 POST 也可以 GET,这时如果用一个 DTO,那么 page、size 这种字段就是数字或者数字字符类型。但是,class-validator 没有提供这种联合类型的校验方法。对此,有热心网友在相关 issue(How to describe validators for two possible types: array or string?) 里提供了解决方案——自定义校验器。
import { registerDecorator } from 'class-validator'; export const isWhether = ( value: string, ...validators: ((value: any) => boolean)[] ) => { return validators.map((validate) => validate(value)).some((valid) => valid); }; export function IsWhether(...validators: [string, (value: any) => boolean][]) { return function (object: any, propertyName: string) { registerDecorator({ name: 'isWhetherType', target: object.constructor, propertyName: propertyName, options: {}, validator: { validate(value: any) { const validatorsFns = validators // eslint-disable-next-line @typescript-eslint/no-unused-vars .map(([_, validate]) => validate) .flat(); return isWhether(value, ...validatorsFns); }, defaultMessage(/* validationArguments?: ValidationArguments */) { const types = validators.map(([type]) => type).flat(); const lastType = types.pop(); if (types.length === 0) return `${propertyName} has to be ${lastType}`; return `${propertyName} can only be ${types.join(', ')} or ${lastType}.`; }, }, }); }; }
JWT
JWT 流行很久了,用起来也还算方便吧,毕竟可以把用户基本信息扔到 token 里,然后直接解出 token 就可以拿到用户信息,省得每次查表。
但是,我还是需要每次查表,因为用户信息是会变动的,比如被禁用了,而已经签发的 token 信息是固定 的,所以,我的 token 里唯一需要的就是用户 id。
解密出来的 sub 为空
jwt 的 payload 里有个 sub 字段(主题),我把用户 id 赋给了这个字段,但是用 verifyAsync 这个方法验证得到的 payload 里 sub 是空的,其他的自定义字段就没问题。后来改成了 decode 方法才拿到需要的 sub。