Prisma 不能优雅的支持 DTO,可以试试 Vona ORM - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zhennann
V2EX    Node.js

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

  •  
  •   zhennann 118 天前 2202 次点击
    这是一个创建于 118 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 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:

    @to() 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  
       118 天前
    作为 Java 过来的

    DTO 真的是恶习。
    zhennann
        2
    zhennann  
    OP
       118 天前
    @yuankui 其实跟标题说的一样,Java 也不能很好的支持自动推断生成 DTO ,所以,开发工作繁重。对于标准的后端 API 而言,DTO 甚至可以说是必须的。因为有了 DTO ,我们就可以支持传入参数的校验,也可以支持 Swagger 元数据的生成。
    ByteCat
        3
    ByteCat  
       118 天前
    直接 drizzle + drizzle-zod 就行了,比 prisma 好用
    zhennann
        4
    zhennann  
    OP
       117 天前
    @ByteCat drizzle-zod 貌似只能基于 table 模型生成 zod schema ,不支持基于 relations 生成 zod schema 。就比如此文中自动生成目录树的 DTO ,drizzle-zod 该如何做呢?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3009 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 13:35 PVG 21:35 LAX 05:35 JFK 08:35
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86