传统 DDD 架构中,一个简单的订单创建流程要经过 5 层转换:
CreateOrderRequest → CreateOrderCommand → Order (Domain) → OrderEntity → OrderResponse每层之间都需要 Mapper,一个订单要写 5 个类 + 4 个 Mapper。我在 Java 项目中多次遇到这个问题,强类型语言做起来确实麻烦。但最近在 Node.js + Prisma + Express 场景下,找到了更优雅的解决方案。
问题根源
传统架构假设:DB 结构 ≠ 领域模型 ≠ API 响应,所以需要多层隔离。但在实际开发中,大部分 CRUD 场景的三者高度相似,强制转换反而制造了大量重复代码。
graph LR A(Client) -->|CreateOrderRequest| B(Controller) B -->|CreateOrderCommand| C(Service) C -->|Order Domain| D(Repository) D -->|OrderEntity| E[(Database)] E -->|OrderEntity| D D -->|Order Domain| C C -->|OrderResponse| B B -->|JSON| A style B fill:#ff6b6b style C fill:#ff6b6b style D fill:#ff6b6b
上图中红色部分都需要 Mapper,代码量是实际业务逻辑的 3-4 倍。
Prisma 如何简化各层模型
| 传统模型层 | Prisma 替代方案 | 说明 |
|---|---|---|
| Domain Model | Prisma Schema 生成的 model | 1. 简单替换 2. 扩展动态字段(Prisma Extension) 3. 完全自定义领域模型 |
| DB Entity | Prisma Schema 生成的 model | 合并为单一真相源 |
| Request DTO | Zod Schema | 校验 + 类型推导 |
| Response DTO | select / pick | 动态控制返回字段 |
核心思路:让 Prisma 承担更多职责,减少胶水代码。
graph LR A(Client) -->|Request| B(Controller) B --> C[Zod] C --> D(Service) D --> E[(Prisma)] E --> D D --> B B -->|select/pick| A style C fill:#51cf66 style D fill:#51cf66
Domain Model:三种简化方式
Prisma Schema 生成的类型可以直接作为领域模型,根据复杂度有 3 种使用方式。
直接使用 Prisma 类型
大部分场景下,Prisma 生成的类型可以直接作为领域模型。
// schema.prisma
model Order {
id String @id @default(uuid())
status OrderStatus
items OrderItem[]
total Decimal
createdAt DateTime @default(now())
@@map("orders")
}
// Prisma 生成的类型 = Domain Model
import { Order } from '@prisma/client'
// Service 直接使用
async function createOrder(data: CreateOrderRequest, userId: string): Promise<Order> {
return prisma.order.create({
data: { status: 'PENDING', total, userId, items: { create: data.items } },
include: { items: true }
})
}如果数据库字段名和代码字段名不同,用 @map 映射:
model Order {
userId String @map("customer_id") // 代码用 userId,DB 存 customer_id
createdAt DateTime @map("created_at")
// ...
}扩展计算字段
用 Prisma Extension 为模型添加计算字段,无需修改数据库。
const prisma = new PrismaClient().$extends({
result: {
order: {
itemCount: {
needs: { items: true },
compute(order) { return order.items.length }
},
canCancel: {
needs: { status: true, createdAt: true },
compute(order) {
const hours = (Date.now() - order.createdAt.getTime()) / 3600000
return order.status === 'PENDING' && hours < 24
}
}
}
}
})
const order = await prisma.order.findUnique({ where: { id }, include: { items: true } })
console.log(order.itemCount) // 自动计算完全自定义领域模型
当需要完全独立的领域模型时(类型不同、复杂业务逻辑),引入自定义类型和 Mapper。
轻量 Mapper 函数
当类型不同(如 Decimal → Money):
import { Order as OrderEntity } from '@prisma/client'
// 自定义领域类型
type Order = {
id: string
customerId: string // 字段名不同
state: OrderState // 枚举类型
totalAmount: Money // 值对象
items: OrderItem[]
}
// 纯函数转换
function toDomain(entity: OrderEntity): Order {
return {
id: entity.id,
customerId: entity.userId,
state: entity.status as OrderState,
totalAmount: new Money(entity.total),
items: entity.items.map(toDomainItem)
}
}
function toEntity(domain: Partial<Order>): Prisma.OrderCreateInput {
return {
userId: domain.customerId!,
status: domain.state,
total: domain.totalAmount!.toDecimal()
}
}对比传统 Mapper 类:纯函数 + 类型推导,代码量减少 70%。
DB Entity:合并为单一真相源
Prisma Schema 生成的类型直接作为数据库 Entity。
// schema.prisma 同时定义了 Domain Model 和 DB Entity
model Order {
id String @id @default(uuid())
status OrderStatus
total Decimal
// ...
}
// 无需单独定义 Entity 类,直接使用 Prisma Client
import { Order } from '@prisma/client'
const order = await prisma.order.create({ data: { status: 'PENDING', total: 100 } })Request DTO:用 Zod 校验和推导
用 Zod 同时完成输入校验和类型定义。
import { z } from 'zod'
const CreateOrderSchema = z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1)
})),
couponCode: z.string().optional()
})
// 自动推导类型
type CreateOrderRequest = z.infer<typeof CreateOrderSchema>
// Controller 中使用
app.post('/orders', async (req, res) => {
const data = CreateOrderSchema.parse(req.body) // 校验 + 解析
const order = await createOrder(data, req.user.id)
res.json({ id: order.id })
})Response DTO:动态控制返回字段
不再手写 Response DTO 类,用 select 或 pick 动态控制返回字段。
Controller 层 pick
app.post('/orders', async (req, res) => {
const order = await createOrder(data, req.user.id)
res.json({
id: order.id,
status: order.status,
total: order.total
})
})Service 层 select
用泛型参数化 select,TypeScript 自动推导返回类型:
async function getOrder<T extends Prisma.OrderSelect>(
orderId: string,
select: T
): Promise<Prisma.OrderGetPayload<{ select: T }>> {
return prisma.order.findUnique({ where: { id: orderId }, select })
}
// 调用方决定返回字段
const publicOrder = await getOrder(id, { id: true, status: true })
const adminOrder = await getOrder(id, {
id: true,
status: true,
user: { select: { email: true } }
})
// 可以显式导出推导的类型
type PublicOrderType = Prisma.OrderGetPayload<{
select: { id: true, status: true }
}>
type AdminOrderType = Prisma.OrderGetPayload<{
select: { id: true, status: true, user: { select: { email: true } } }
}>视图投影函数
如果返回结构完全不同:
const OrderView = {
forCustomer(order: Order) {
return {
id: order.id,
state: order.state,
total: order.totalAmount.format(),
items: order.items.map(item => ({
name: item.productName,
price: item.salePrice.format()
}))
}
},
forAdmin(order: Order) {
return {
...this.forCustomer(order),
cost: order.totalCost.format(),
profit: order.profit.format()
}
}
}
res.json(OrderView.forCustomer(order))渐进式引入复杂度
- Prisma Schema替换 + Zod + select —> 解决大部分场景
- Prisma Extension → 解决大部分自定义的问题
- 自定义model + 轻量 Mapper → 解决复杂的问题
这样既保留了 Prisma 的开发效率,又获得了领域层的表达能力,且不会陷入 Mapper 地狱。