新公司的项目中用了Egg和MongoDB及mongoose。最近有空来系统的梳理一下。
参考资料&扩展阅读:
Egg 使用 egg-mongoose来方便的跟数据库进行链接
// eggProject/server/config/config.default.ts
// 单数据库链接
mongoose: {
url: process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017/laurel',
},
// 多数据库链接
mongoose: {
clients: {
// clientId, access the client instance by app.mongooseDB.get('clientId')
// 数据库1
db1: {
url: process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017/db1',
options: {},
},
// 数据库2
db2: {
url: process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017/db2',
options: {},
},
},
},
// 多数据库链接时,在model中要指定是哪个
module.exports = app => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;
const conn = app.mongooseDB.get('db1');
const UserSchema = new Schema({
userName: { type: String },
password: { type: String },
});
return conn.model('User', UserSchema);
}
export default app => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;
// 连接多个数据库时要在这里指定是哪个数据库
const connect = app.mongooseDB.get('db1');
const UserSchema = new Schema(
{
name: { type: String },
account: { type: String, unique: true }, // 唯一
password: { type: String },
comments: [{ body: String, date: Date }], // 对象数组
tags: { type: [String], index: true } // 字符串数组,对字段增加索引
meta: { // 对象
votes: Number,
favs: Number
},
h: { type: String, alias: 'home' }, // 别名,使用时无缝连接
},
// schema 的选项options
{
usePushEach: true, // 解决 Unknown modifier: $pushAll 报错,作用原理未知
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' }, // 生成时间
},
);
// 还可以定义实例方法,定义方法时 不要使用 箭头函数。
UserSchema.method.someFun = function(cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
// mongoose会自动给下边的user加上s,也就是说真正的表名应该是users
return connect.model('user', UserSchema);
// 如果不想这样,可以写第三个参数手动指定表名如下,这样表名就是user了。
return connect.model('user', UserSchema, 'user');
};
Virtuals 是 document 的属性,但是不会被保存到 MongoDB,所以也不能用来查询。 getter 可以用于格式化和组合字段数据, setter 可以很方便地分解一个值到多个字段。
personSchema.virtual('fullName').
// 定义一个虚拟值 fullName,他并不会被保存到数据库里,只会把first和last拼接出来返回
get(function() { return this.name.first + ' ' + this.name.last; }).
// 定义虚拟值如果是 setter,会把值自动拆分到其他属性
set(function(v) {
this.name.first = v.substr(0, v.indexOf(' '));
this.name.last = v.substr(v.indexOf(' ') + 1);
});
注意:如果对 document 使用 toJSON() 或 toObject(),默认不包括虚拟值, 你需要额外向 toObject() 或者 toJSON() 传入参数 { virtuals: true }。
Schemas 有很多可配置选项,你可以在构造时传入或者直接 set:
new Schema({..}, options);
// or
var schema = new Schema({..});
schema.set(option, value);
常见选项:
id
,指向数据库的_id
,但会转成字符串返回SchemaType 处理字段路径各种属性的定义
以下是 mongoose 的所有合法 SchemaTypes:
// 可以简单的直接写类型
var schema1 = new Schema({
test: String // `test` is a path of type String
});
// 也可以写个对象,type表示类型。这样还能加其他属性
var schema2 = new Schema({
test: {
type: String,
lowercase: true, // 比如存入数据库时都转成小写字母
}
});
全部字段可用的选项:
索引相关:
对字符串:
对数字:
对日期:
常用方法:
详见:Mongoose中文文档-Mongoose的API
用于构建聚合管道的聚合构造函数。通过
Model.aggregate()
构造使用。
详见: Aggregate
Aggregate能够在查询数据时做各种复杂的操作,包括查询,过滤,排序,分组,增加字段组合计算等等,十分强大!下面是例子。
但是,下面的例子中,如果addFields中增加的字段很通用,其实可以试着使用Schema中的虚拟值(Virtual),通过设置setter和getter也能达到了类似的效果,并且写法也比这个更友好。这是我梳理总结时思考出来的,在之后的开发中,要尽可能使用更简单的方式。
const result = await ctx.model.xxxx.aggregate([
{
$match: { type: 'recharge' }, // 匹配出符合条件的数据
},
{
$sort: { createdAt: -1 }, // 按数据生成时间倒序排列
},
{
$addFields: { // 增加项:
// payAmount 用户支付的金额 = 数量 amount * 价格 price * 订单折扣 discount
payAmount: { $multiply: ['$amount', '$price', '$discount'] },
// waitUnfreezeAmount 待解冻本金 = amount * 锁仓率 freezeRatio - 已解冻本金 $unfreezeAmount
waitUnfreezeAmount: {
$subtract: [{ $multiply: ['$amount', '$freezeRatio'] }, '$unfreezeAmount'],
},
// interest 已产生利息 = 已解冻利息 unfreezeInterest + 待解冻利息 waitUnfreezeInterest
interest: { $add: ['$unfreezeInterest', '$waitUnfreezeInterest'] },
},
},
// 根据币种分组,求和
{
$group: {
_id: '$coin',
totalPayPerCoin: {
// 该币种的支付数量总数
$sum: '$payAmount',
},
totalInterestPerCoin: {
// 该币种的已产生利息
$sum: '$interest',
},
waitUnfreezePerCoin: {
// 该币种的待解冻本金
$sum: '$waitUnfreezeAmount',
},
unfreezePerCoin: {
// 该币种的待解冻本金
$sum: '$unfreezeAmount',
},
totalLUDPerCoin: {
// 该币种兑换的总数
$sum: '$amount',
},
countPerCoin: {
// 该币种参与兑换总人数
$sum: 1,
},
},
},
]);