⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

2.5.4 和数据库打交道的层——数据层

一句话破题

数据层是应用与数据库之间的"桥梁"——它封装了所有数据访问逻辑,让上层不需要关心数据是怎么存、怎么取的。

数据层的职责边界

应该做不应该做
数据的增删改查业务逻辑判断
复杂 SQL/查询权限校验
事务控制发送通知
数据转换调用外部服务

Repository 模式

基本结构

typescript
// repositories/post.repository.ts
import { prisma } from '@/lib/prisma'
import type { Prisma } from '@prisma/client'

export const postRepository = {
  // 基础 CRUD
  async findById(id: string) {
    return prisma.post.findUnique({
      where: { id },
      include: { author: true, tags: true },
    })
  },
  
  async findMany(params: {
    page: number
    pageSize: number
    authorId?: string
    status?: string
  }) {
    const { page, pageSize, authorId, status } = params
    
    const where: Prisma.PostWhereInput = {}
    if (authorId) where.authorId = authorId
    if (status) where.status = status
    
    const [posts, total] = await Promise.all([
      prisma.post.findMany({
        where,
        skip: (page - 1) * pageSize,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: { author: true },
      }),
      prisma.post.count({ where }),
    ])
    
    return { posts, total, page, pageSize }
  },
  
  async create(data: Prisma.PostCreateInput) {
    return prisma.post.create({
      data,
      include: { author: true },
    })
  },
  
  async update(id: string, data: Prisma.PostUpdateInput) {
    return prisma.post.update({
      where: { id },
      data,
      include: { author: true },
    })
  },
  
  async delete(id: string) {
    return prisma.post.delete({ where: { id } })
  },
}

业务查询方法

typescript
// repositories/post.repository.ts
export const postRepository = {
  // ...基础 CRUD
  
  // 业务相关的查询
  async findPublished() {
    return prisma.post.findMany({
      where: { status: 'published' },
      orderBy: { publishedAt: 'desc' },
    })
  },
  
  async findByAuthor(authorId: string) {
    return prisma.post.findMany({
      where: { authorId },
      orderBy: { createdAt: 'desc' },
    })
  },
  
  async countTodayByAuthor(authorId: string) {
    const today = new Date()
    today.setHours(0, 0, 0, 0)
    
    return prisma.post.count({
      where: {
        authorId,
        createdAt: { gte: today },
      },
    })
  },
  
  async incrementViewCount(id: string) {
    return prisma.post.update({
      where: { id },
      data: { viewCount: { increment: 1 } },
    })
  },
  
  async findWithRelations(id: string) {
    return prisma.post.findUnique({
      where: { id },
      include: {
        author: true,
        tags: true,
        comments: {
          include: { author: true },
          orderBy: { createdAt: 'desc' },
        },
      },
    })
  },
}

事务处理

简单事务

typescript
// repositories/order.repository.ts
async createWithItems(
  orderData: Prisma.OrderCreateInput,
  items: Prisma.OrderItemCreateManyInput[]
) {
  return prisma.$transaction(async (tx) => {
    const order = await tx.order.create({ data: orderData })
    
    await tx.orderItem.createMany({
      data: items.map(item => ({ ...item, orderId: order.id })),
    })
    
    return order
  })
}

带事务参数的方法

typescript
// repositories/product.repository.ts
async decreaseStock(
  tx: Prisma.TransactionClient,
  productId: string,
  quantity: number
) {
  return tx.product.update({
    where: { id: productId },
    data: { stock: { decrement: quantity } },
  })
}

// 使用
await prisma.$transaction(async (tx) => {
  await productRepository.decreaseStock(tx, productId, quantity)
  await orderRepository.create(tx, orderData)
})

复杂查询

关联查询

typescript
async findPostsWithStats(authorId: string) {
  return prisma.post.findMany({
    where: { authorId },
    include: {
      _count: {
        select: {
          comments: true,
          likes: true,
        },
      },
    },
  })
}

聚合查询

typescript
async getAuthorStats(authorId: string) {
  return prisma.post.aggregate({
    where: { authorId },
    _count: { id: true },
    _sum: { viewCount: true },
    _avg: { viewCount: true },
  })
}

分组查询

typescript
async getPostCountByStatus(authorId: string) {
  return prisma.post.groupBy({
    by: ['status'],
    where: { authorId },
    _count: { id: true },
  })
}

数据转换

typescript
// repositories/user.repository.ts
import type { User } from '@prisma/client'
import type { UserDTO } from '@/types/user'

function toDTO(user: User): UserDTO {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    avatar: user.avatar,
    createdAt: user.createdAt.toISOString(),
    // 不返回密码等敏感字段
  }
}

export const userRepository = {
  async findById(id: string): Promise<UserDTO | null> {
    const user = await prisma.user.findUnique({ where: { id } })
    return user ? toDTO(user) : null
  },
}

觉知:数据层常见问题

1. 在 Repository 里写业务逻辑

typescript
// ❌ 权限判断是业务逻辑
async findById(id: string, userId: string) {
  const post = await prisma.post.findUnique({ where: { id } })
  if (post.authorId !== userId) {
    throw new Error('无权限')  // 这是业务逻辑!
  }
  return post
}

// ✅ Repository 只负责取数据
async findById(id: string) {
  return prisma.post.findUnique({ where: { id } })
}
// 权限判断放在 Service 层

2. 查询条件硬编码

typescript
// ❌ 查询条件写死
async findPosts() {
  return prisma.post.findMany({
    where: { status: 'published' },  // 硬编码
  })
}

// ✅ 通过参数传入
async findPosts(params: { status?: string }) {
  const where: Prisma.PostWhereInput = {}
  if (params.status) where.status = params.status
  return prisma.post.findMany({ where })
}

3. N+1 查询问题

typescript
// ❌ 循环中查询,导致 N+1
const posts = await prisma.post.findMany()
for (const post of posts) {
  post.author = await prisma.user.findUnique({
    where: { id: post.authorId }
  })
}

// ✅ 使用 include 一次查询
const posts = await prisma.post.findMany({
  include: { author: true }
})

Prisma 最佳实践

1. 单例模式

typescript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

2. 查询日志

typescript
const prisma = new PrismaClient({
  log: process.env.NODE_ENV === 'development' 
    ? ['query', 'error', 'warn'] 
    : ['error'],
})

3. 软删除

typescript
// 中间件实现软删除
prisma.$use(async (params, next) => {
  if (params.model === 'Post') {
    if (params.action === 'delete') {
      params.action = 'update'
      params.args.data = { deletedAt: new Date() }
    }
    if (params.action === 'findMany') {
      params.args.where = { ...params.args.where, deletedAt: null }
    }
  }
  return next(params)
})

本节小结

原则说明
只管数据Repository 只负责数据访问
参数化查询查询条件通过参数传入
避免 N+1使用 include 关联查询
事务封装复杂操作使用事务保证一致性