Skip to content

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 })

下一步