V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zhennann
V2EX  ›  Node.js

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

  •  
  •   zhennann · 29 天前 · 1290 次点击

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

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

    限于篇幅,这里不展开讲解 Vona ORM 所有的知识点,而是以目录树为例,演示如何查询一棵目录树,以及如何动态生成 DTO ,并最终生成 Swagger 元数据。

    1. 创建 Entity

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

    @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:声明为可选字段

    2. 创建 Model

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

    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 对多来创建目录的自身引用关系。

    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 的代码骨架:

    @Controller()
    export class ControllerCategory extends BeanBase {}
    

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

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

    5. 查询目录树

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

    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的类型推断效果:

    1.png

    2.png

    6. 自动推断 DTO

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

    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 效果:

    3.png

    4.png

    5.png

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

    7. 封装 DTO

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

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

    @Dto()
    export class DtoCategoryTree {}
    

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

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

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

    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[]>
    yuankui
        1
    yuankui  
       29 天前
    作为 Java 过来的

    DTO 真的是恶习。
    zhennann
        2
    zhennann  
    OP
       29 天前
    @yuankui 其实跟标题说的一样,Java 也不能很好的支持自动推断生成 DTO ,所以,开发工作繁重。对于标准的后端 API 而言,DTO 甚至可以说是必须的。因为有了 DTO ,我们就可以支持传入参数的校验,也可以支持 Swagger 元数据的生成。
    ByteCat
        3
    ByteCat  
       29 天前
    直接 drizzle + drizzle-zod 就行了,比 prisma 好用
    zhennann
        4
    zhennann  
    OP
       28 天前
    @ByteCat drizzle-zod 貌似只能基于 table 模型生成 zod schema ,不支持基于 relations 生成 zod schema 。就比如此文中自动生成目录树的 DTO ,drizzle-zod 该如何做呢?
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3258 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 11:02 · PVG 19:02 · LAX 04:02 · JFK 07:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.