传统 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 ModelPrisma Schema 生成的 model1. 简单替换
2. 扩展动态字段(Prisma Extension)
3. 完全自定义领域模型
DB EntityPrisma Schema 生成的 model合并为单一真相源
Request DTOZod Schema校验 + 类型推导
Response DTOselect / 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 函数

当类型不同(如 DecimalMoney):

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 类,用 selectpick 动态控制返回字段。

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 地狱。