Repository 模式
Repository 模式是 DoraCMS 的核心设计模式,它将数据访问逻辑从业务逻辑中分离出来,提供统一的数据操作接口。
什么是 Repository 模式?
Repository 模式是一种数据访问层的设计模式,它在数据源层和业务逻辑层之间添加了一个抽象层。
传统 MVC 的问题
javascript
// ❌ 传统方式:业务代码直接操作数据库
class UserService {
async getUsers() {
// MongoDB 版本
return await User.find({ status: 'active' })
.populate('role')
.sort({ createdAt: -1 })
.limit(10)
// 如果切换到 MariaDB,需要完全重写
// return await User.findAll({
// where: { status: 'active' },
// include: [{ model: Role }],
// order: [['createdAt', 'DESC']],
// limit: 10
// })
}
}问题:
- 业务代码与数据库强耦合
- 切换数据库需要重写所有查询
- 查询逻辑分散,难以维护
- 无法复用代码
Repository 模式的解决方案
javascript
// ✅ Repository 模式:统一的数据访问接口
class UserService {
async getUsers() {
// MongoDB 和 MariaDB 使用完全相同的代码
return await this.userRepository.findMany({
filters: { status: 'active' },
populate: ['role'],
sort: { createdAt: -1 },
pagination: { limit: 10 }
})
}
}优势:
- ✅ 业务代码与数据库解耦
- ✅ 切换数据库无需修改业务代码
- ✅ 查询逻辑统一封装
- ✅ 代码复用率 90%+
Repository vs Service
很多人会混淆 Repository 和 Service 的职责,让我们明确一下它们的区别:
| 维度 | Repository 层 | Service 层 |
|---|---|---|
| 职责 | 数据访问 | 业务逻辑 |
| 操作对象 | 单个数据表/集合 | 多个 Repository |
| 事务管理 | 不涉及 | 负责管理 |
| 业务规则 | 不包含 | 负责实现 |
| 依赖关系 | 依赖数据库 | 依赖 Repository |
| 可复用性 | 高度复用 | 业务特定 |
示例对比
javascript
// Repository 层:只负责数据访问
class UserRepository extends BaseStandardRepository {
// 数据查询
async findByEmail(email) {
return await this.findOne({ filters: { email } })
}
// 数据验证
async checkEmailUnique(email, excludeId) {
return await this.checkUnique({ email }, excludeId)
}
}
// Service 层:负责业务逻辑编排
class UserService {
// 用户注册业务逻辑
async register(userData) {
// 1. 验证邮箱唯一性(使用 Repository)
const emailExists = await this.userRepository.checkEmailUnique(userData.email)
if (emailExists) {
throw new Error('邮箱已被注册')
}
// 2. 加密密码(业务逻辑)
const encryptedPassword = await this.cryptoUtil.encryptPassword(userData.password)
// 3. 创建用户(使用 Repository)
const user = await this.userRepository.create({
...userData,
password: encryptedPassword
})
// 4. 发送欢迎邮件(业务逻辑)
await this.mailService.sendWelcomeEmail(user.email)
// 5. 记录日志(业务逻辑)
await this.logService.log('user.register', user.id)
return user
}
}Repository 架构层次
DoraCMS 的 Repository 采用四层继承结构:
┌─────────────────────────────────────────────┐
│ IBaseRepository (接口层) │
│ · 定义标准接口 │
│ · 类型约束 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ BaseStandardRepository (跨数据库基类) │
│ · 通用 CRUD 操作 │
│ · 统一参数接口 │
│ · 异常处理 │
│ · 钩子方法 │
└────────┬──────────────────────┬──────────────┘
│ │
┌────────▼──────────┐ ┌────────▼──────────┐
│ BaseMongoRepository│ │BaseMariaRepository│
│ · MongoDB 实现 │ │ · MariaDB 实现 │
│ · Mongoose 集成 │ │ · Sequelize 集成 │
└────────┬──────────┘ └────────┬──────────┘
│ │
┌────────▼──────────┐ ┌────────▼──────────┐
│UserMongoRepository│ │UserMariaRepository│
│ · 用户特定逻辑 │ │ · 用户特定逻辑 │
└───────────────────┘ └───────────────────┘第一层:IBaseRepository (接口层)
定义标准接口和类型约束:
typescript
interface IBaseRepository<T> {
// 查询操作
findById(id: string, options?: QueryOptions): Promise<T>
findOne(params: QueryParams): Promise<T>
findMany(params: QueryParams): Promise<T[]>
findByPage(params: QueryParams): Promise<PageResult<T>>
// 创建操作
create(data: Partial<T>): Promise<T>
createMany(data: Partial<T>[]): Promise<T[]>
// 更新操作
updateById(id: string, data: Partial<T>): Promise<T>
updateMany(filters: any, data: Partial<T>): Promise<number>
// 删除操作
deleteById(id: string): Promise<boolean>
deleteMany(filters: any): Promise<number>
// 统计操作
count(filters: any): Promise<number>
}第二层:BaseStandardRepository (跨数据库基类)
实现通用逻辑,定义钩子方法:
javascript
class BaseStandardRepository {
// 通用查询方法
async findMany(params) {
// 1. 参数转换(由子类实现)
const query = this._buildQuery(params)
// 2. 执行查询(由子类实现)
let result = await this._executeQuery(query)
// 3. 数据处理(调用钩子)
result = await this._postprocessData(result)
return result
}
// 钩子方法(供子类重写)
_getDefaultPopulate() { return [] }
_getDefaultSort() { return { createdAt: -1 } }
_customProcessDataItem(item) { return item }
// 抽象方法(子类必须实现)
_buildQuery(params) { throw new Error('Must implement') }
_executeQuery(query) { throw new Error('Must implement') }
}第三层:BaseMongoRepository / BaseMariaRepository
实现数据库特定逻辑:
javascript
class BaseMongoRepository extends BaseStandardRepository {
_buildQuery(params) {
const { filters, populate, sort, fields } = params
let query = this.model.find(filters || {})
// 处理关联查询
if (populate?.length) {
populate.forEach(p => query = query.populate(p))
}
// 处理排序
if (sort) query = query.sort(sort)
// 处理字段选择
if (fields?.length) query = query.select(fields.join(' '))
return query
}
async _executeQuery(query) {
return await query.exec()
}
}javascript
class BaseMariaRepository extends BaseStandardRepository {
_buildQuery(params) {
const { filters, populate, sort, fields } = params
const options = {
where: filters || {},
include: [],
order: [],
attributes: fields
}
// 处理关联查询
if (populate?.length) {
populate.forEach(p => {
options.include.push({ association: p })
})
}
// 处理排序
if (sort) {
Object.entries(sort).forEach(([field, order]) => {
options.order.push([field, order === 1 ? 'ASC' : 'DESC'])
})
}
return options
}
async _executeQuery(options) {
return await this.model.findAll(options)
}
}第四层:具体 Repository
实现业务特定逻辑:
javascript
class UserMongoRepository extends BaseMongoRepository {
constructor(app) {
super(app, app.model.User) // 注入模型
}
// 重写钩子:配置默认关联查询
_getDefaultPopulate() {
return ['role', 'department']
}
// 重写钩子:配置默认排序
_getDefaultSort() {
return { createdAt: -1, userName: 1 }
}
// 重写钩子:数据处理
_customProcessDataItem(user) {
// 排除密码字段
if (user.password) {
delete user.password
}
return user
}
// 业务特定方法
async findByEmail(email) {
return await this.findOne({
filters: { email }
})
}
async checkEmailUnique(email, excludeId) {
return await this.checkUnique({ email }, excludeId)
}
async updateLastLogin(userId) {
return await this.updateById(userId, {
lastLoginAt: new Date()
})
}
}统一参数接口
DoraCMS 定义了一套统一的参数接口,屏蔽了不同数据库的差异:
QueryParams 查询参数
typescript
interface QueryParams {
// 查询条件
filters?: {
[key: string]: any
}
// 关联查询
populate?: string[] | PopulateOption[]
// 排序
sort?: {
[field: string]: 1 | -1 | 'asc' | 'desc'
}
// 字段选择
fields?: string[]
// 分页
pagination?: {
page?: number
limit?: number
skip?: number
}
// 搜索
search?: {
keyword?: string
fields?: string[]
}
}使用示例
javascript
// ✅ 统一的查询方式(MongoDB 和 MariaDB 通用)
const users = await userRepository.findMany({
// 查询条件
filters: {
status: 'active',
age: { $gte: 18 } // 大于等于 18
},
// 关联查询
populate: ['role', 'department'],
// 排序
sort: {
createdAt: -1, // 按创建时间倒序
userName: 1 // 按用户名正序
},
// 字段选择
fields: ['id', 'userName', 'email', 'role'],
// 分页
pagination: {
page: 1,
limit: 10
},
// 搜索
search: {
keyword: 'john',
fields: ['userName', 'email', 'nickName']
}
})核心方法
查询操作
javascript
// 根据 ID 查询
const user = await userRepository.findById('507f1f77bcf86cd799439011')
// 查询单条记录
const admin = await userRepository.findOne({
filters: { role: 'admin', status: 'active' }
})
// 查询多条记录
const users = await userRepository.findMany({
filters: { department: 'IT' },
populate: ['role'],
sort: { createdAt: -1 }
})
// 分页查询
const result = await userRepository.findByPage({
filters: { status: 'active' },
pagination: { page: 1, limit: 20 }
})
// result = { list: [...], total: 100, page: 1, limit: 20 }
// 统计数量
const count = await userRepository.count({ status: 'active' })创建操作
javascript
// 创建单条记录
const user = await userRepository.create({
userName: 'john',
email: 'john@example.com',
password: 'hashed_password'
})
// 批量创建
const users = await userRepository.createMany([
{ userName: 'user1', email: 'user1@example.com' },
{ userName: 'user2', email: 'user2@example.com' }
])更新操作
javascript
// 根据 ID 更新
const user = await userRepository.updateById('507f1f77bcf86cd799439011', {
status: 'inactive'
})
// 批量更新
const count = await userRepository.updateMany(
{ department: 'IT' }, // 条件
{ status: 'active' } // 更新数据
)删除操作
javascript
// 根据 ID 删除
const success = await userRepository.deleteById('507f1f77bcf86cd799439011')
// 批量删除
const count = await userRepository.deleteMany({
status: 'deleted',
createdAt: { $lt: new Date('2023-01-01') }
})钩子方法
Repository 提供了丰富的钩子方法,让你可以自定义行为:
配置钩子
javascript
class UserRepository extends BaseMongoRepository {
// 默认关联查询
_getDefaultPopulate() {
return ['role', 'department']
}
// 默认排序
_getDefaultSort() {
return { createdAt: -1 }
}
// 默认搜索字段
_getDefaultSearchKeys() {
return ['userName', 'email', 'nickName']
}
// 状态字段映射
_getStatusMapping() {
return {
field: 'status',
values: {
active: 1,
inactive: 0,
deleted: -1
}
}
}
// 有效字段(MariaDB)
_getValidTableFields() {
return ['id', 'userName', 'email', 'password', 'status']
}
// 排除字段(MariaDB)
_getExcludeTableFields() {
return ['createdBy', 'updatedBy']
}
}数据处理钩子
javascript
class UserRepository extends BaseMongoRepository {
// 创建前处理
_customPreprocessForCreate(data) {
// 设置默认值
return {
...data,
status: data.status || 'active',
createdAt: new Date()
}
}
// 更新前处理
_customPreprocessForUpdate(data) {
// 移除不允许更新的字段
delete data.createdAt
delete data.createdBy
// 设置更新时间
return {
...data,
updatedAt: new Date()
}
}
// 数据项处理
_customProcessDataItem(user) {
// 排除敏感字段
if (user.password) {
delete user.password
}
// 格式化数据
if (user.createdAt) {
user.createdAtFormatted = formatDate(user.createdAt)
}
return user
}
// 批量数据后处理
async _postprocessData(data) {
if (Array.isArray(data)) {
// 批量处理
return data.map(item => this._customProcessDataItem(item))
} else {
// 单条处理
return this._customProcessDataItem(data)
}
}
}实际应用示例
用户模块
javascript
// UserMongoRepository.js
class UserMongoRepository extends BaseMongoRepository {
constructor(app) {
super(app, app.model.User)
}
_getDefaultPopulate() {
return ['role', 'department']
}
_customProcessDataItem(user) {
// 排除密码
if (user.password) delete user.password
return user
}
// 业务方法
async findByEmail(email) {
return await this.findOne({ filters: { email } })
}
async findByUserName(userName) {
return await this.findOne({ filters: { userName } })
}
async checkEmailUnique(email, excludeId) {
return await this.checkUnique({ email }, excludeId)
}
async updateLastLogin(userId) {
return await this.updateById(userId, {
lastLoginAt: new Date(),
loginCount: { $inc: 1 }
})
}
}
// UserService.js
class UserService {
async getUserList(params) {
// 直接使用 Repository
return await this.userRepository.findByPage({
filters: params.filters,
populate: ['role', 'department'],
sort: { createdAt: -1 },
pagination: {
page: params.page,
limit: params.limit
}
})
}
async login(userName, password) {
// 1. 查询用户
const user = await this.userRepository.findByUserName(userName)
if (!user) {
throw new Error('用户不存在')
}
// 2. 验证密码
const valid = await this.cryptoUtil.verifyPassword(password, user.password)
if (!valid) {
throw new Error('密码错误')
}
// 3. 更新登录时间
await this.userRepository.updateLastLogin(user.id)
// 4. 生成 Token
const token = this.jwtUtil.sign({ userId: user.id })
return { user, token }
}
}文章模块
javascript
class ArticleRepository extends BaseMongoRepository {
_getDefaultPopulate() {
return ['author', 'category', 'tags']
}
_getDefaultSort() {
return { publishedAt: -1, createdAt: -1 }
}
_getDefaultSearchKeys() {
return ['title', 'summary', 'content']
}
// 查询已发布文章
async findPublished(params) {
return await this.findMany({
...params,
filters: {
...params.filters,
status: 'published',
publishedAt: { $lte: new Date() }
}
})
}
// 查询热门文章
async findHot(limit = 10) {
return await this.findMany({
filters: { status: 'published' },
sort: { viewCount: -1, likeCount: -1 },
pagination: { limit }
})
}
// 增加浏览数
async incrementView(articleId) {
return await this.updateById(articleId, {
$inc: { viewCount: 1 }
})
}
}优势总结
1. 代码复用率 90%+
所有 CRUD 操作都在基类中实现,具体 Repository 只需要实现业务特定逻辑。
2. 数据库切换零成本
javascript
// 配置文件一行代码切换数据库
config.dbType = 'mongodb' // 或 'mariadb'
// 业务代码完全不需要修改
const users = await userRepository.findMany({
filters: { status: 'active' }
})3. 统一的开发规范
所有模块都遵循相同的开发规范,降低学习成本,提高开发效率。
4. 易于测试
Repository 层可以轻松 Mock,便于单元测试:
javascript
// 测试 Service 时 Mock Repository
const mockUserRepository = {
findByEmail: jest.fn().mockResolvedValue({ id: '1', email: 'test@example.com' })
}
const userService = new UserService({ userRepository: mockUserRepository })