前言
在开发过程中是不是碰到这样一个场景,数据库直接获取某个对象的时候,需要额外生成一个参数给客户端,例如:file
文件给用户时实际上要额外返回一个签名url,这个是不能从数据库直接获取的,这时候就可以通过typeorm
提供的 subscribe
订阅监听来解决了
更多可以参考 这里
案例
只需要新建一个类,继承自 EntitySubscriberInterface
,且使用 EventSubscriber
类装饰器,只需要设置监听 entity
和 实现回调
即可,这里实现的是加载 entity
后的回调 afterLoad
,也就是我们查询生成对象的之后会进行回调,使用非常方便
实际使用可以不仅仅在加载后添加内容,还可以在 insert、remove 前后等做操作,具体可以点进接口查看
import { DataSource, EntitySubscriberInterface, EventSubscriber } from 'typeorm'
//监听
@EventSubscriber()
export class FileSubscriber implements EntitySubscriberInterface<File> {
constructor(dataSource: DataSource, private minioServer: MinioService) {
dataSource.subscribers.push(this)
}
listenTo() {
return File
}
async afterLoad(entity: File) {
//需要注意的是,如果存在关联,关联为空时,也会返回entity,只不过为 {id: null}的形式,这是需要注意处理的
if (!entity.filename) return
//假设我们使用 minio 对文件进行签名期限url
entity.url = await this.minioServer.getPresignedUrl(entity.filename)
}
}
//数据库 file entity
@Entity()
export class File {
@PrimaryGeneratedColumn('uuid')
id: string
@Column('bigint', { default: 0 })
size: number
@Column({ nullable: true, default: null })
filename: string
@Column({ default: null })
mimetype: string
@CreateDateColumn()
timestamp: Date
//返回时获取的url,实际数据库不存在
url: string
}
//modules 中间需要导入,不然不生效哈
providers: [FileSubscriber]
优缺点
优点很明显,使用方便了很多,不用频繁对我们查询的结果赋值了,能节省很多代码,且能专对 entity 操作前后进行我们额外的操作,用起来屡试不爽,且这是 typeorm 内部编写,因此支持最好,因此可以立即为比较适合 typeorm 的操作方式
缺点同时也很明显,我们的 entity 需要增加额外的参数,该监听也会与我们的 entity 耦合到一起,entity 不再纯粹对应关系数据库了,不管我们需不需要额外的参数,获取时一定会额外获取增加的参数,查询结果不再是单纯对应我们编写的 dto 了(甚至可能让你有了不想编写返回值文档的想法😂)
ps
:如果 typeorm 能够在查询时,可以根据我们添加的参数选择是否使用监听那就更好了
简易版监听(@AfterLoad)
上面就是一个稍微重点的例子了,只能使用 subscribe 了(要导入 service,这里面直接导入 service 会出现一些问题,因此没办法直接写到 entity 中)
像下面的,可能只需要一个小小的加工即可生成,那么直接而使用 @AfterLoad
然后编写内容即可,这样就可以生成我们想要的 url 了
//数据库 file entity
@Entity()
export class File {
@PrimaryGeneratedColumn('uuid')
id: string
@Column('bigint', { default: 0 })
size: number
@Column({ nullable: true, default: null })
filename: string
@Column({ default: null })
mimetype: string
@CreateDateColumn()
timestamp: Date
//返回时获取的url,实际数据库不存在
url: string
@AfterLoad()
generateUrl() {
this.url = 'askdfjaskdhfaksdhfakhl'
}
}
AfterLoad扩展
如果我们就是懒得用上面的订阅,就想使用简单的 @AfterLoad
怎么办,没问题,就这个例子来说,我们假设使用到了 minioService
,MinioService
一般也不会多个模块同时导入,则可以直接使用单例 + 异步
解决,这样其他模块使用时该 service 的部分功能,都不用直接导入了,可以直接使用单例
//我们创建一个单例获取 minioService
let minioService: MinioService | null = null
export class MinioService {
...
constructor() {
...
minioService = this
}
static get share() {
//单例,用于跨场景使用,最好不要多个模块导入该Service
return minioService
}
...
//加入一个案例
getTestUrl(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我是测试返回的假的url')
}, 100);
})
}
}
再通过 AfterLoad
给我们的 url 赋值即可
url: string
@AfterLoad()
async generateUrl() {
this.url = await MinioService.share.getTestUrl()
}
subscribe 小技巧(请权衡利弊后使用)
通过小技巧,将 entity 中额外增加的参数分离到 dto,可避免 entity 中出现新的参数,订阅时,将参数声明成 dto 的类型即可
//监听
@EventSubscriber()
export class FileSubscriber implements EntitySubscriberInterface<FileDto> {
constructor(dataSource: DataSource, private minioServer: MinioService) {
dataSource.subscribers.push(this)
}
listenTo() {
return File
}
async afterLoad(entity: FileDto) {
//假设我们使用 minio 对文件进行签名期限url
entity.url = await this.minioServer.getPresignedUrl(entity.filename)
}
}
//数据库 file entity,此时url就不存在了
@Entity()
export class File {
@PrimaryGeneratedColumn('uuid')
id: string
@Column('bigint', { default: 0 })
size: number
@Column({ nullable: true, default: null })
filename: string
@Column({ default: null })
mimetype: string
@CreateDateColumn()
timestamp: Date
}
原因
:
我们的 typescript 最终也会被编译成 javascript,且 typeorm 的仓库都是 js + d.ts 的声明方式出现,意味着里面都是 js 编写的,因此里面实际生成 entity 的时候是没有类型限制的,那时候新增加一个 url,并不会出现错误(这就是解释型语言的好处),因此我们可以将类型声明成我们的返回类型dto,那么就可以实现 entity 与返回结果分离了(实际查询 entity 过程中仍然会额外返回 url)
为什么这么做?
我们的 dto 实际上的参数,才是实际上对应这返回给客户端的参数,除了我们新增的 url,什么一对一,一对多这种,如果根本不会反向关联,在返回时 dto 中实际都不会存在该参数,因此我们这个小技巧更像是专门为返回给用户的 dto 为主而做的,并且 dto 更像是我们的 entity 派生而来(实际上不一定是继承关系,可能就是entity与dto基础参数组合的原型函数,他们存在继承关系)
因此这个参数我们再返回 entity 类型时使用,类型上并不可见(理解为派生类,因此实际存在),返回给用户确实时该派生类,符合面向对象的多态
扩展
有人可能觉得,虽然用着很方便,甚至提到了类似派生类的方式来解释,但仍然无法避免这个增加的参数,且每次查询都要额外生成,如果这个参数不常用,且很消耗性能,那样会造成不必要的消耗,这样怎么办呢
只想说,用最原始的办法,生成返回的 dto 类型时,dto 额外增加一个参数,返回数据时手动给该参数赋值,如果是数组对象,则需要访问并赋值
想直接传参就可以选择是否使用监听,那么告诉编写 typeorm 的团队更新一下,一劳永逸(还是老老实实手动赋值吧)