Skip to content

双数据库支持

DoraCMS 的核心特性之一是支持 MongoDB 和 MariaDB 双数据库,且可以无缝切换,无需修改业务代码。

为什么支持双数据库?

业务场景

不同的应用场景对数据库有不同的需求:

场景推荐数据库原因
内容管理MongoDB灵活的文档结构,适合非结构化数据
电商系统MariaDB强事务支持,数据一致性要求高
数据分析MariaDB复杂查询,多表关联
实时数据MongoDB高并发写入,水平扩展

技术优势

  • 灵活性 - 根据业务需求选择最合适的数据库
  • 平滑迁移 - 可以在开发过程中切换数据库
  • 学习成本低 - 一套代码掌握两种数据库
  • 降低风险 - 不被单一数据库厂商绑定

如何实现双数据库支持?

1. Repository/Adapter 模式

通过 Repository 模式 + Adapter 适配器实现数据库抽象:

┌─────────────────────────────────────────┐
│         业务代码(统一接口)             │
└──────────────────┬──────────────────────┘

┌──────────────────▼──────────────────────┐
│       BaseStandardRepository            │
│         (跨数据库基类)                   │
└────────┬──────────────────┬─────────────┘
         │                  │
┌────────▼─────────┐ ┌──────▼──────────────┐
│BaseMongoRepository│ │BaseMariaRepository  │
│   (MongoDB 适配)  │ │   (MariaDB 适配)    │
└──────────────────┘ └─────────────────────┘

2. 统一参数接口

定义一套统一的参数接口,屏蔽不同数据库的语法差异:

javascript
// 统一的查询接口
const users = await userRepository.findMany({
  filters: { status: 'active' },      // 查询条件
  populate: ['role'],                 // 关联查询
  sort: { createdAt: -1 },           // 排序
  fields: ['id', 'name', 'email'],   // 字段选择
  pagination: { page: 1, limit: 10 } // 分页
})

3. Adapter 查询转换

Adapter 负责将统一接口转换为特定数据库的查询:

javascript
// MongoDB 转换
{
  filters: { status: 'active' }
} 

{ status: 'active' }

// 关联查询
{ populate: ['role'] }

.populate('role')

// 排序
{ sort: { createdAt: -1 } }

.sort({ createdAt: -1 })
javascript
// MariaDB 转换
{
  filters: { status: 'active' }
}

{ where: { status: 'active' } }

// 关联查询
{ populate: ['role'] }

{ include: [{ association: 'role' }] }

// 排序
{ sort: { createdAt: -1 } }

{ order: [['createdAt', 'DESC']] }

数据库配置

MongoDB 配置

1. 安装依赖

bash
pnpm add mongoose egg-mongoose

2. 启用插件

编辑 config/plugin.js:

javascript
exports.mongoose = {
  enable: true,
  package: 'egg-mongoose',
}

3. 配置连接

编辑 config/config.default.js:

javascript
config.mongoose = {
  client: {
    url: 'mongodb://127.0.0.1:27017/doracms',
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      poolSize: 10,
    },
  },
}

// 设置数据库类型
config.dbType = 'mongodb'

4. 定义 Schema

javascript
// app/model/user.js
module.exports = app => {
  const mongoose = app.mongoose
  const Schema = mongoose.Schema
  
  const UserSchema = new Schema({
    userName: { type: String, required: true, unique: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
    status: { type: String, default: 'active' },
    role: { type: Schema.Types.ObjectId, ref: 'Role' },
    createdAt: { type: Date, default: Date.now }
  })
  
  return mongoose.model('User', UserSchema)
}

MariaDB 配置

1. 安装依赖

bash
pnpm add sequelize egg-sequelize mariadb

2. 启用插件

编辑 config/plugin.js:

javascript
exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
}

3. 配置连接

编辑 config/config.default.js:

javascript
config.sequelize = {
  dialect: 'mariadb',
  host: '127.0.0.1',
  port: 3306,
  database: 'doracms',
  username: 'root',
  password: 'password',
  timezone: '+08:00',
  pool: {
    max: 10,
    min: 0,
    acquire: 30000,
    idle: 10000,
  },
}

// 设置数据库类型
config.dbType = 'mariadb'

4. 定义 Model

javascript
// app/model/user.js
module.exports = app => {
  const { STRING, DATE, ENUM } = app.Sequelize
  
  const User = app.model.define('user', {
    id: { type: STRING(24), primaryKey: true },
    userName: { type: STRING(50), allowNull: false, unique: true },
    email: { type: STRING(100), allowNull: false, unique: true },
    password: { type: STRING(100), allowNull: false },
    status: { type: ENUM('active', 'inactive'), defaultValue: 'active' },
    roleId: { type: STRING(24) },
    createdAt: { type: DATE, defaultValue: Date.now }
  })
  
  // 定义关联
  User.associate = function() {
    app.model.User.belongsTo(app.model.Role, { 
      foreignKey: 'roleId',
      as: 'role'
    })
  }
  
  return User
}

切换数据库

配置文件切换

只需修改配置文件中的 dbType 即可:

javascript
// config/config.default.js

// 使用 MongoDB
config.dbType = 'mongodb'

// 或使用 MariaDB
config.dbType = 'mariadb'

环境变量切换

bash
# .env 文件
DB_TYPE=mongodb  # 或 mariadb
javascript
// config/config.default.js
config.dbType = process.env.DB_TYPE || 'mongodb'

Repository 工厂

Repository 工厂根据配置自动选择正确的 Repository:

javascript
// app/extend/context.js
module.exports = {
  get repo() {
    if (!this[REPO]) {
      const dbType = this.app.config.dbType
      this[REPO] = {}
      
      // 根据数据库类型加载 Repository
      if (dbType === 'mongodb') {
        this[REPO].user = new UserMongoRepository(this.app)
        this[REPO].article = new ArticleMongoRepository(this.app)
      } else if (dbType === 'mariadb') {
        this[REPO].user = new UserMariaRepository(this.app)
        this[REPO].article = new ArticleMariaRepository(this.app)
      }
    }
    return this[REPO]
  }
}

业务代码零修改

javascript
// Service 层代码完全不需要修改
class UserService extends Service {
  async getUserList(params) {
    // MongoDB 和 MariaDB 使用完全相同的代码
    return await this.ctx.repo.user.findByPage({
      filters: params.filters,
      populate: ['role'],
      sort: { createdAt: -1 },
      pagination: { page: params.page, limit: params.limit }
    })
  }
}

字段映射

ID 字段差异

不同数据库的 ID 字段名不同:

数据库ID 字段名类型
MongoDB_idObjectId
MariaDBidString

DoraCMS 自动处理:

javascript
// 查询时自动转换
await userRepository.findById('507f1f77bcf86cd799439011')

// MongoDB: 查询 { _id: ObjectId('507f1f77bcf86cd799439011') }
// MariaDB: 查询 { id: '507f1f77bcf86cd799439011' }

// 返回数据统一为 id
// { id: '507f1f77bcf86cd799439011', userName: 'john' }

时间字段格式

javascript
// MongoDB: Date 对象
{ createdAt: ISODate("2024-01-01T00:00:00.000Z") }

// MariaDB: DATETIME 字符串
{ createdAt: "2024-01-01 00:00:00" }

// DoraCMS 自动转换为统一格式
{ createdAt: "2024-01-01T00:00:00.000Z" }

JSON 字段处理

javascript
// MongoDB: 原生支持对象
{
  settings: {
    theme: 'dark',
    language: 'zh-CN'
  }
}

// MariaDB: JSON 字符串需要转换
{
  settings: '{"theme":"dark","language":"zh-CN"}'
}

// DoraCMS 自动处理 JSON 字段
// _getJsonFields() { return ['settings', 'config'] }

关联查询

MongoDB 关联查询

javascript
// MongoDB: populate
const users = await userRepository.findMany({
  populate: ['role', 'department']
})

// 实际执行
User.find().populate('role').populate('department')

MariaDB 关联查询

javascript
// MariaDB: include
const users = await userRepository.findMany({
  populate: ['role', 'department']
})

// 实际执行
User.findAll({
  include: [
    { association: 'role' },
    { association: 'department' }
  ]
})

深度关联

javascript
// 统一接口
const articles = await articleRepository.findMany({
  populate: [
    'author',
    {
      path: 'comments',
      populate: ['user']
    }
  ]
})

查询操作符

统一操作符

DoraCMS 定义了统一的查询操作符:

操作符说明MongoDBMariaDB
$eq等于$eq=
$ne不等于$ne!=
$gt大于$gt>
$gte大于等于$gte>=
$lt小于$lt<
$lte小于等于$lte<=
$in在数组中$inIN
$nin不在数组中$ninNOT IN
$like模糊查询$regexLIKE

使用示例

javascript
// 统一的查询语法
const users = await userRepository.findMany({
  filters: {
    age: { $gte: 18, $lt: 60 },           // 18 <= age < 60
    status: { $in: ['active', 'pending'] }, // status in (...)
    userName: { $like: 'john' },           // userName like '%john%'
    email: { $ne: null }                   // email is not null
  }
})

事务处理

MongoDB 事务

javascript
// MongoDB 事务(需要 Replica Set)
const session = await mongoose.startSession()
session.startTransaction()

try {
  await userRepository.create(userData, { session })
  await userConfigRepository.create(configData, { session })
  await session.commitTransaction()
} catch (error) {
  await session.abortTransaction()
  throw error
} finally {
  session.endSession()
}

MariaDB 事务

javascript
// MariaDB 事务
const transaction = await sequelize.transaction()

try {
  await userRepository.create(userData, { transaction })
  await userConfigRepository.create(configData, { transaction })
  await transaction.commit()
} catch (error) {
  await transaction.rollback()
  throw error
}

性能对比

MongoDB 优势

  • ✅ 灵活的文档结构
  • ✅ 高并发写入
  • ✅ 水平扩展容易
  • ✅ 原生支持数组和对象

MariaDB 优势

  • ✅ 强事务支持
  • ✅ 复杂查询性能好
  • ✅ 数据一致性强
  • ✅ 成熟的生态系统

性能测试数据

操作MongoDBMariaDB
简单查询~1ms~1ms
复杂查询~5ms~3ms
写入~0.5ms~1ms
批量写入~50ms (1000条)~100ms (1000条)
深度关联~10ms~5ms

数据迁移

MongoDB → MariaDB

bash
# 1. 导出 MongoDB 数据
mongoexport --db doracms --collection users --out users.json

# 2. 转换数据格式
node scripts/convert-mongo-to-maria.js

# 3. 导入 MariaDB
node scripts/import-to-maria.js

MariaDB → MongoDB

bash
# 1. 导出 MariaDB 数据
mysqldump -u root -p doracms users > users.sql

# 2. 转换数据格式
node scripts/convert-maria-to-mongo.js

# 3. 导入 MongoDB
mongoimport --db doracms --collection users --file users.json

最佳实践

1. 根据场景选择数据库

javascript
// 内容管理 → MongoDB
// 灵活的文档结构,适合非结构化数据
config.dbType = 'mongodb'

// 电商系统 → MariaDB
// 强事务支持,保证数据一致性
config.dbType = 'mariadb'

2. 避免数据库特定语法

javascript
// ❌ 使用数据库特定语法
const users = await User.find({ age: { $gte: 18 } })  // MongoDB only

// ✅ 使用统一接口
const users = await userRepository.findMany({
  filters: { age: { $gte: 18 } }
})

3. 合理使用关联查询

javascript
// ❌ N+1 查询
const users = await userRepository.findMany({})
for (const user of users) {
  user.role = await roleRepository.findById(user.roleId)
}

// ✅ 使用关联查询
const users = await userRepository.findMany({
  populate: ['role']
})

4. 注意数据类型

javascript
// MongoDB: ObjectId vs String
// MariaDB: 统一使用 String

// ✅ 统一使用字符串 ID
const user = await userRepository.findById('507f1f77bcf86cd799439011')

常见问题

Q: 如何选择数据库?

A: 根据业务需求选择:

  • 需要灵活的数据结构 → MongoDB
  • 需要强事务支持 → MariaDB
  • 需要复杂查询 → MariaDB
  • 需要高并发写入 → MongoDB

Q: 切换数据库需要改代码吗?

A: 不需要。只需修改配置文件中的 dbType

Q: 可以同时使用两个数据库吗?

A: 可以。配置两个数据源,在 Repository 中指定使用哪个。

Q: 性能有影响吗?

A: Repository 模式的抽象层对性能影响很小(< 1ms),相比收益可以忽略不计。

下一步