Prisma不能优雅的支持DTO,试试Vona ORM吧
作为Node.js生态中最流行的ORM之一,Prisma凭借其简洁的schema定义和自动生成的类型安全查询API,赢得了大量开发者的青睐。但当你开始构建复杂业务系统时,会发现一个尴尬的痛点:DTO(数据传输对象)的支持简直是一场灾难 。今天,我们来聊聊为什么Prisma在DTO面前如此"笨拙",以及Vona ORM如何优雅地解决这个问题。## Prisma的DTO之痛:你只能"手动"处理DTO的核心职责是定义API层的数据结构,它往往与数据库实体不同。比如,用户表有password字段,但API响应中绝不能返回它;商品表有createdAt,但客户端不需要看到时间戳。Prisma的模型定义直接映射数据库表,所有字段都会被查询出来(除非你手动select),这意味着你必须在每个查询里手写字段过滤,或者在service层一遍遍做对象映射。看这个Prisma的典型场景:typescript// prisma/schema.prismamodel User { id Int @id @default(autoincrement()) email String @unique password String // 敏感字段 name String createdAt DateTime @default(now())}// 假设我们有个API端点:GET /users/:id// 在controller/service层,你必须手动剔除password和createdAtconst user = await prisma.user.findUnique({ where: { id: userId }, // 只select需要的字段,手动列出来,写起来累,还容易漏 select: { id: true, email: true, name: true }});// 或者更糟:全查出来再手动删const rawUser = await prisma.user.findUnique({ where: { id: userId } });const dto = { id: rawUser.id, email: rawUser.email, name: rawUser.name};这还算能忍。但一旦你有嵌套关联 (比如获取用户+他的订单),Prisma的select嵌套会变得冗长无比;如果字段有几十个,写select简直是一场灾难。更关键的是,Prisma没有内置的"DTO类"概念,你无法复用这些字段选择逻辑,每个查询都要重新写一遍,维护成本极高。## Vona ORM:让DTO成为一等公民Vona ORM(一个现代TypeScript/JavaScript ORM)的设计哲学就是"让数据转换变得声明式"。它在核心层面支持DTO类定义 ,你只需要写一次DTO,就可以在查询、序列化、反序列化中自动复用,无需手写select或map。### 核心特性:@Dto() 与自动字段过滤Vona允许你定义DTO类,并用装饰器标记它与数据库模型的关系。当你用DTO作为查询目标时,Vona会自动只获取DTO声明的字段,并自动排除敏感字段。typescript// 1. 定义数据库模型(类似Prisma schema)import { Model, Field } from 'vona-orm';@Model()class User { @Field({ primary: true }) id: number; @Field() email: string; @Field({ secret: true }) // 标记为敏感,不会出现在DTO中 password: string; @Field() name: string; @Field({ hidden: true }) // 内部字段,不对外 createdAt: Date;}// 2. 定义UserDTO ------ 只暴露需要的字段import { Dto } from 'vona-orm';@Dto(User) // 关联到User模型class UserDto { id: number; email: string; name: string; // 注意:没有password和createdAt,Vona自动忽略它们}// 3. 查询时直接指定DTO ------ Vona自动只查询DTO字段const userDto = await orm.findUnique(UserDto, { where: { id: 1 } });// 自动生成SQL: SELECT id, email, name FROM users WHERE id = 1// 不会查询password,也不会查询createdAt ------ 安全、高效console.log(userDto);// 输出: { id: 1, email: 'test@example.com', name: 'Test' }看到区别了吗?你不需要手写select,不需要手动剔除password。Vona通过@Dto装饰器将数据库模型与传输对象绑定,字段过滤是自动的。而且这个DTO类可以在任何地方复用------比如在create和update接口中,你可以用它作为请求体的类型校验。### 嵌套DTO与自动关联映射再来看一个更复杂的场景:用户和他的订单列表。在Prisma中,你需要嵌套select,并且要反复定义类型。Vona则通过DTO的嵌套自动处理:typescript// 定义Order模型@Model()class Order { @Field({ primary: true }) id: number; @Field() userId: number; @Field() total: number; @Field({ hidden: true }) internalNote: string;}// 定义OrderDTO@Dto(Order)class OrderDto { id: number; total: number;}// 定义UserWithOrdersDTO ------ 包含嵌套的订单DTO@Dto(User)class UserWithOrdersDto { id: number; email: string; name: string; // 关联字段:Vona会自动查询该用户的所有订单,并映射为OrderDto数组 @Relation(() => OrderDto, { via: 'userId' }) orders: OrderDto[];}// 查询用户及其订单 ------ 一条语句搞定,且只取需要的字段const userWithOrders = await orm.findUnique(UserWithOrdersDto, { where: { id: 1 }, include: { orders: true } // 显式声明需要关联});// 自动生成SQL: // SELECT id, email, name FROM users WHERE id = 1// SELECT id, total FROM orders WHERE userId = 1console.log(userWithOrders);// 输出: { id: 1, email: 'test@example.com', name: 'Test', orders: [{ id: 101, total: 99.9 }, { id: 102, total: 29.9 }] }注意几点:- internalNote字段在OrderDto中不存在,所以Vona不会查询它。- 嵌套关联通过@Relation装饰器声明,不需要手写select嵌套。- 类型安全:userWithOrders的类型就是UserWithOrdersDto,完全匹配。## 为什么Vona能"优雅"而Prisma不行?根本原因在于设计理念 :- Prisma 将"数据库模型"和"API结构"混为一谈。它的模型定义就是数据库表结构,查询结果就是数据库行的直接映射。DTO作为"另一层"概念,需要开发者额外实现,而Prisma的API设计并没有为"对象转换"提供内置支持,导致你只能手动拼字段。- Vona 从第一天起就认识到"数据库结构 ≠ 对外结构"。它提供了@Dto装饰器来显式声明"对外暴露什么",并且将字段过滤、嵌套映射、敏感数据隐藏等逻辑内建在查询引擎中 。你只需要定义DTO,剩下的交给ORM。## 实战对比:一个简单的REST API让我们用同一个场景(获取用户信息)分别用Prisma和Vona写一遍,感受差异:Prisma版本: typescript// controller.tsimport { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export async function getUser(id: number) { // 手动select,还要确保不泄露password const user = await prisma.user.findUnique({ where: { id }, select: { id: true, email: true, name: true, profile: { select: { bio: true } } } // 嵌套也要写 }); return user;}Vona版本: typescript// dto.tsimport { Dto, Model, Field } from 'vona-orm';@Model() class User { /* ...字段定义... */ }@Model() class Profile { /* ...字段定义... */ }@Dto(User)class UserDto { id: number; email: string; name: string; @Relation(() => ProfileDto, { via: 'userId' }) profile: ProfileDto;}@Dto(Profile)class ProfileDto { bio: string;}// controller.tsexport async function getUser(id: number) { return await orm.findUnique(UserDto, { where: { id } });}Vona的代码量少了30%以上,而且UserDto可以复用于其他接口(比如POST /users的请求体校验),无需重复定义。## 总结Prisma是一个优秀的ORM,它在schema迁移和类型安全查询方面做得很好。但它的架构限制使得DTO支持变得"笨拙"------你必须手动管理字段选择、嵌套映射,并且代码难以复用。而Vona ORM通过将DTO作为一等公民,提供了声明式的字段过滤、自动嵌套关联、敏感数据隐藏 ,让你专注于业务逻辑而非数据转换。如果你正在开发一个API密集型应用,受够了手写select和DTO映射,不妨试试Vona ORM。它可能不是最流行的,但它在"优雅地处理数据传输对象"这件事上,确实走在了Prisma前面。当然,选择ORM还要考虑生态、迁移、性能等因素,但如果你重视DTO的清晰表达,Vona绝对值得一试。