Prisma不能优雅的支持DTO,试试Vona ORM吧

在Nodejs生态中,Prisma是一个非常流行的ORM库,支持Typescript,提供了非常友好的类型推断能力。但是,Prisma却不能优雅的支持DTO。在与其他后端框架整合时,DTO是进行参数验证、生成Swagger元数据的关键节点。如果不能像推断类型一样自动推断出DTO,那么,我们就仍然需要手工创建DTO。随着业务的增长,复杂的表间关系会让手工补充DTO的工作日益繁重。

而Vona ORM就提供了非常便利的工具,使我们可以非常直观的动态推断出DTO,就像推断类型一样,从而解放我们的双手,显著提升生产力。甚至可以说,能够自动推断DTO,为Nodejs后端框架打开了一扇窗。

Vona本身就是一款更直观的Nodejs框架,如果大家第一次接触,可以先参考这篇文章:你认为Vonajs提供的这些特性会比Nestjs更好用吗?

限于篇幅,这里不展开讲解Vona ORM所有的知识点,而是以目录树为例,演示如何查询一棵目录树,以及如何动态生成DTO,并最终生成Swagger元数据。Vona框架作者正在直播撰写Vona文档。围观官方文档的实时编写过程,有利于加深对框架设计的理解,探索不一样的架构设计路径。有兴趣的欢迎移步:B站濮水代码直播间

1. 创建Entity

在VSCode中,可以通过右键菜单Vona Create/Entity创建Entity的代码骨架:

typescript 复制代码
@Entity('demoStudentCategory')
export class EntityCategory extends EntityBase {
  @Api.field()
  name: string;

  @Api.field(v.optional())
  categoryIdParent?: TableIdentity;
}
  • 行2: 继承自EntityBase,就会自动提供5个基础字段:id、createdAt、updatedAt、deleted、iid
    • iid:是实例Id,通过多实例的机制支持多租户系统的开发
  • 行4、7: 定义两个业务字段:name、categoryIdParent
  • @Api.field:通过此装饰器定义的信息,可同时应用于参数验证和Swagger元数据
  • v.optional:声明为可选字段
  • 更多信息,参见:Vona Entity

2. 创建Model

在VSCode中,可以通过右键菜单Vona Create/Model创建Model的代码骨架:

typescript 复制代码
import { EntityCategory } from '../entity/category.ts';

@Model({ entity: EntityCategory })
export class ModelCategory extends BeanModelBase<EntityCategory> {}
  • 行3: entity:指定Model所对应的Entity
  • 行4: 继承自BeanModelBase,从而拥有大量操作数据库的方法,如:CRUD、聚合、分组,等等

3. 创建树形结构

如果要创建一棵目录树,本质就是建立Model引用自身的递归结构。Vona ORM同样支持4种关系:1对11对多多对1多对多。那么,在这里,我们就需要采用1对多来创建目录的自身引用关系。

diff 复制代码
import { EntityCategory } from '../entity/category.ts';

@Model({
  entity: EntityCategory,
+ relations: {
+   children: $relation.hasMany(() => ModelCategory, 'categoryIdParent', {
+     autoload: true,
+     columns: ['id', 'name'],
+   }),
+ },
})
export class ModelCategory extends BeanModelBase<EntityCategory> {}
  • 行5: relations:可以定义多个关系
  • 行6: children:定义一个1对多的关系
  • $relation.hasMany:
    • 参数1: 引用自身ModelCategory
    • 参数2: 设置关联外键categoryIdParent
    • 参数3: 关联选项
      • autoload:是否自动加载关联数据
      • columns:控制关联数据的字段列表

4. 创建Controller

在VSCode中,可以通过右键菜单Vona Create/Controller创建Controller的代码骨架:

typescript 复制代码
@Controller()
export class ControllerCategory extends BeanBase {}

接下来我们创建一个Api,用于获取目录树:

typescript 复制代码
export class ControllerCategory extends BeanBase {
  @Web.get('getCategoryTree')
  async getCategoryTree() {
  }
}
  • 行2: 通过@Web.get定义一个api,path为getCategoryTree

5. 查询目录树

一般而言,我们还需要创建一个Service,从而实现以下调用链:Controller->Service->Model->操作数据库。为了简化起见,在这里,我们直接在Controller中调用Model方法:

typescript 复制代码
export class ControllerCategory extends BeanBase {
  @Web.get('getCategoryTree')
  async getCategoryTree() {
    const tree = await this.scope.model.category.select({
      columns: ['id', 'name'],
    });
    return tree;
  }
}
  • 行4: 通过this.scope取得Category Model,然后调用select方法
    • columns:指定要查询的字段列表

由于前面我们设置children关系为autoload: true,因此,查询结果tree就是一棵完整的目录树。下面我们看一下tree的类型推断效果:

6. 自动推断DTO

现在我们自动推断DTO,并且设为API的返回数据的类型:

diff 复制代码
export class ControllerCategory extends BeanBase {
  @Web.get('getCategoryTree')
+ @Api.body(v.array(v.object($Dto.get(() => ModelCategory, { columns: ['id', 'name'] }))))
  async getCategoryTree() {
    const tree = await this.scope.model.category.select({
      columns: ['id', 'name'],
    });
    return tree;
  }
}
  • 行3: 通过@Api.body定义API返回数据的类型:
    • v.array:定义数组类型
    • v.object:定义对象类型
  • $Dto.get:动态推断DTO
    • 参数1:指定目标Model
    • 参数2:指定选项
      • columns:指定要提取的字段列表

同样,由于前面我们设置children关系为autoload: true,因此,$Dto.get生成的DTO就是一棵完整的目录树。下面我们看一下API的Swagger效果:

从示意图中,我们可以清晰的看到,这棵树引用的children类型是名称为demo-student.entity.category_2c7d642ee581efa300341e343180fbb0ecdc785d的动态Entity的数组,从而形成一种递归的引用关系。

7. 封装DTO

虽然我们已经实现了预期的目标,但是Vona ORM提供的能力还没有结束。我们可以创建一个新的DTO,将前面的代码$Dto.get(() => ModelCategory, { columns: ['id', 'name'] })封装起来,从而用于其他地方:

在VSCode中,可以通过右键菜单Vona Create/Dto创建DTO的代码骨架:

typescript 复制代码
@Dto()
export class DtoCategoryTree {}

然后我们通过继承机制来封装DTO:

diff 复制代码
@Dto()
export class DtoCategoryTree 
+ extends $Dto.get(() => ModelCategory, { columns: ['id', 'name'] }) {}

现在,我们再使用新创建的DTO来改造前面的API代码:

diff 复制代码
export class ControllerCategory extends BeanBase {
  @Web.get('getCategoryTree')
+ @Api.body(v.array(v.object(DtoCategoryTree)))
+ async getCategoryTree(): Promise<DtoCategoryTree[]>{
    const tree = await this.scope.model.category.select({
      columns: ['id', 'name'],
    });
    return tree;
  }
}
  • 行3: 直接传入DtoCategoryTree
  • 行4: 返回类型为Promise<DtoCategoryTree[]>

结语

Vonajs已开源:github.com/vonajs/vona

Vonajs作者正在B站直播撰写技术文档,工作日每晚8:30,欢迎围观:濮水代码直播间

相关推荐
野区小女王2 小时前
react调用接口渲染数据时,这些表格里的数据是被禁选的
前端·react.js·前端框架
Wang's Blog2 小时前
Nestjs框架: Node.js 多环境配置策略与 dotenv 与 config 库详解
node.js
啃火龙果的兔子4 小时前
Node.js (Express) + MySQL + Redis构建项目流程
mysql·node.js·express
WindrunnerMax6 小时前
浅谈 RAG 并基于 NodeJS 实现基础向量检索服务
架构·node.js·aigc
用户15186530413846 小时前
从传统办公软件到云协作Flash Table AI分钟级生成表单,打造企业远程高效率办公的利器
前端·后端·前端框架
香蕉可乐荷包蛋7 小时前
node.js常用函数
node.js
chancygcx_18 小时前
前端框架Vue3(二)——Vue3核心语法之OptionsAPI与CompositionAPI与setup
vue.js·前端框架
止观止19 小时前
Remix框架:高性能React全栈开发实战
前端·react.js·前端框架·remix
萌萌哒草头将军20 小时前
🚀🚀🚀 深入探索 Node.js v22.18.0 新特性;默认支持运行 ts 文件了!
前端·typescript·node.js