双数据库支持
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-mongoose2. 启用插件
编辑 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 mariadb2. 启用插件
编辑 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 # 或 mariadbjavascript
// 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 | _id | ObjectId |
| MariaDB | id | String |
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 定义了统一的查询操作符:
| 操作符 | 说明 | MongoDB | MariaDB |
|---|---|---|---|
$eq | 等于 | $eq | = |
$ne | 不等于 | $ne | != |
$gt | 大于 | $gt | > |
$gte | 大于等于 | $gte | >= |
$lt | 小于 | $lt | < |
$lte | 小于等于 | $lte | <= |
$in | 在数组中 | $in | IN |
$nin | 不在数组中 | $nin | NOT IN |
$like | 模糊查询 | $regex | LIKE |
使用示例
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 优势
- ✅ 强事务支持
- ✅ 复杂查询性能好
- ✅ 数据一致性强
- ✅ 成熟的生态系统
性能测试数据
| 操作 | MongoDB | MariaDB |
|---|---|---|
| 简单查询 | ~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.jsMariaDB → 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),相比收益可以忽略不计。
下一步
- 📖 Repository 模式 - 深入理解实现原理
- 🏗️ 三层架构 - 了解架构设计
- 🚀 快速开始 - 开始使用 DoraCMS
- 💡 最佳实践 - 新模块开发指南