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 关联查询 |
| 事务封装 | 复杂操作使用事务保证一致性 |
