如何将Drizzle Query Builder结果映射为嵌套对象?
从NestJS-TypeORM迁移至Drizzle ORM:Query Builder结果转嵌套格式的最佳实践
我正从NestJS-TypeORM迁移至Drizzle ORM,遇到以下差异:
- TypeORM的仓库与查询构建器返回结构相似的结果,便于处理关联数据。
- Drizzle中,
db.query()返回规整的嵌套数据(比如用户及其关联帖子的分组结构),但Query Builder(db.select())返回原始扁平数据,需要额外处理才能实现类似嵌套结构。
示例对比
db.query()返回结果
[ { "id": 10, "name": "Dan", "posts": [ { "id": 1, "content": "SQL is awesome", "authorId": 10 }, { "id": 2, "content": "But check relational queries", "authorId": 10 } ] } ]
Query Builder返回结果
const result: { user: { id: number; name: string }; pets: { id: number; name: string; ownerId: number } | null; }[] = await db .select() .from(users) .leftJoin(pets, eq(users.id, pets.ownerId));
我希望找到一种机制或最佳实践,将Query Builder的结果映射为嵌套格式(类似db.query()的输出),保证API响应一致,无需客户端适配不同格式。请问有哪些建议、模式或库推荐?在Drizzle项目中是如何处理此问题的?
解决方案建议
1. 自定义分组映射函数
这是最直接的方式,手动遍历扁平结果,按关联主键分组构建嵌套结构,适合简单关联场景,无需额外依赖。
示例代码(用户-宠物关联):
interface FlatUserPet { user: { id: number; name: string }; pets: { id: number; name: string; ownerId: number } | null; } interface NestedUser { id: number; name: string; pets: { id: number; name: string; ownerId: number }[]; } function mapToNestedUsers(flatResults: FlatUserPet[]): NestedUser[] { const userMap = new Map<number, NestedUser>(); for (const item of flatResults) { const { user, pets } = item; // 初始化用户条目 if (!userMap.has(user.id)) { userMap.set(user.id, { ...user, pets: [] }); } // 添加关联宠物(排除null情况) if (pets) { userMap.get(user.id)!.pets.push(pets); } } return Array.from(userMap.values()); } // 使用方式 const flatResult = await db.select().from(users).leftJoin(pets, eq(users.id, pets.ownerId)); const nestedResult = mapToNestedUsers(flatResult);
2. 复用Drizzle的关联定义逻辑
如果必须使用Query Builder(比如需要复杂自定义SQL),可以结合Drizzle的relations实体关系定义,参考db.query的内部分组逻辑,封装通用映射工具。db.query本质是执行SQL后自动完成分组嵌套,你可以提取这部分逻辑复用,避免重复造轮子。
3. 第三方数据转换库简化复杂场景
对于多层级关联(如用户-帖子-评论),手动写映射函数会繁琐,可借助专门的转换库:
- Zod:兼顾数据校验与格式转换,通过
transform方法实现扁平到嵌套的转换
示例:import { z } from 'zod'; const flatUserPetSchema = z.object({ user: z.object({ id: z.number(), name: z.string() }), pets: z.object({ id: z.number(), name: z.string(), ownerId: z.number() }).nullable() }); const nestedUserSchema = z.object({ id: z.number(), name: z.string(), pets: z.array(z.object({ id: z.number(), name: z.string(), ownerId: z.number() })) }); function mapToNestedUsersWithZod(flatResults: z.infer<typeof flatUserPetSchema>[]) { const userMap = new Map<number, z.infer<typeof nestedUserSchema>>(); for (const item of flatResults) { const user = item.user; if (!userMap.has(user.id)) { userMap.set(user.id, { ...user, pets: [] }); } if (item.pets) { userMap.get(user.id)!.pets.push(item.pets); } } return nestedUserSchema.array().parse(Array.from(userMap.values())); } - Class-transformer:结合NestJS生态,通过DTO类装饰器完成转换
示例:import { Transform, Type, plainToInstance } from 'class-transformer'; class PetDto { id: number; name: string; ownerId: number; } class UserDto { id: number; name: string; @Transform(({ obj }) => obj.pets ? [obj.pets] : [], { toClassOnly: true }) @Type(() => PetDto) pets: PetDto[]; } // 使用方式 const flatResult = await db.select().from(users).leftJoin(pets, eq(users.id, pets.ownerId)); const grouped = mapToNestedUsers(flatResult); const nestedResult = plainToInstance(UserDto, grouped);
4. 封装NestJS查询复用层
在NestJS项目中,可封装DrizzleQueryService自定义服务,提供selectWithNested方法,内部执行Query Builder查询后自动调用映射函数,统一处理转换逻辑,避免重复代码。
项目实际处理方式
在我维护的Drizzle项目中:
- 简单关联场景用自定义映射函数,轻便灵活;
- 复杂关联场景结合Zod做校验+转换,保证数据正确性;
- 性能敏感场景直接用
db.query,复用其优化后的自动分组逻辑,减少额外处理成本。
内容的提问来源于stack exchange,提问作者BereketeAb B. Kebebew
相关产品推荐
相关产品推荐

