猫鼬是一种看似温顺,但实际上很厉害的小动物,据说性情凶暴起来足以杀死一条眼镜蛇。在程序员的世界里,猫鼬竟然也占有重要一席。最近接触的两个开源项目,都以 mongoose(猫鼬的英文)命名,可见其受喜爱的程度。
这两只猫鼬聪明敏捷,执行任务非常出色,已经是我不可或缺的小伙伴。我打算分两次来介绍一下它们,首先要出场的这只猫鼬,是为使用 NodeJS+MongoDB 的程序猿准备的。
mongoose 官网首页的一句话把 mongoose 的用途交代的很清楚:
Let’s face it, writing MongoDB validation, casting and business logic boilerplate is a drag. That’s why we wrote Mongoose.
说白了 mongoose 就是一个 ORM,如果你要做的东西很简单,就别用 mongoose 折腾了,推荐使用更加简易的monk。
不过就 GitHub 上 star 的数量来说,mongoose 是 monk 十倍有余,流行程度遥遥领先,所以本文就聊一聊这个了不起的 mongoose。
mongoose 有三个最重要的概念:Schema, Model, instance.
因为 Schema 和 Model 用作类型(可理解为 class),为了强调这一点,我给 Schema 和 Model 的首字母用了大写,instance 是对象实例,所以首字母小写。
下面用一个例子串起它们三个的用法。我用关系型数据库的语言描述一下这个例子,先有个了解,以便更有目的性地看代码:
我们数据库里保存的,是菜市场里的菜价信息。有两个表,一个表是 markets(菜市场),字段包含市场的名字和地理位置。另一个表是 prices(菜价),字段包含某天某种菜在某个市场的价格,其中表示市场的_market 字段指向 markets 表中的某一条记录,可理解为外键。
下面逐块代码进行讲解:
var mongoose = require("mongoose");
var events = require("events");
var emitter = new events.EventEmitter();
var Schema = mongoose.Schema;
function handleError(err) {
console.log("error: ", err);
}
mongoose.connect("mongodb://localhost/test");
我们的例子由一连串增删改查(CRUD)的操作组成,首先是 create,等到 create 成功以后才进行下一步 read,然后才 update 和 delete。由于这些数据库操作都是异步返回的,所以要等前一个操作完成后再进行下一个操作。以上代码中的 events,就是为了串行化而引入的。在下面的例子中,由 EventEmitter 来负责串行化的事件触发。
除了 events 和 emitter 这两行,剩下的几乎是使用 mongoose 所必备的开篇代码,无需多言。
Schema
MongoDB 是 document 数据库,本质上不对数据格式做限制。但是这不妨碍我们人为地去规范格式,比如我们希望某个 collection 中的元素都拥有相同的字段和数据类型,单纯而美好的愿望。
Schema 就是干这用的。除了限制类型,Schema 还可以定义 method。所有脱胎于这个 Schema 的对象,都有了相同的格式和 method。
// 定义Schema
var priceSchema = new Schema({
date: { type: Date, index: true },
name: String,
_market: { type: Schema.Types.ObjectId, ref: "Market" },
min: Number,
max: Number,
avg: Number,
unit: { type: String, default: "元/千克" },
tags: [String],
});
var marketSchema = new Schema({
name: String,
lat: Number,
lng: Number,
province: String,
city: String,
district: String,
street: String,
StreetNumber: String,
});
priceSchema 有一个_market 字段值得注意,使用下划线开头是为了强调它的特殊性。ref: ‘Market’表示它引用的类型,也就是说这个字段是一个以 Market 为 model 生成的 instance。 上面的例子里,所有字段的类型都是基础类型,嵌套类型如何表达呢?
nestedTypeField: {
x: {
y: Number,
z: [String]
}
}
其实这个字段的类型是匿名的 Model,对已有的 Model,直接把 Model 作为类型即可。
Model
按照习惯上对 ORM 的理解,不就只有类和对象吗?类对应着表定义,对象对应着表中的记录。但是 mongoose 却整出来三个概念,Schema 对应着表定义,instance 对应着表记录,Model 是干啥的?
可将 Model 理解成 Schema 和 instance 之间的粘合剂,Schema 生 Model,Model 生 instance。然而从日常用途来看,主要是通过 Model 来增删改查。
首先需要以一个 Schema 为模板生成 Model,然后拿这个 Model 去 create, find, remove, update.
// 由Schema创建Model
var Price = mongoose.model("Price", priceSchema);
var Market = mongoose.model("Market", marketSchema);
instance
一个 instance 就是一条数据,也就是 collection 中的一个 document。
CRUD 操作
create
向数据库插入数据,一种方法是生成 instance,然后调用 instance 上的 save 方法。另一种是直接调用 Model 上的 create 方法。
// create的示例代码
function create() {
// 准备数据
var marketJson = {
name: "抚顺路蔬菜副食品批发市场",
lat: 36.103964,
lng: 120.38536,
province: "山东省",
city: "青岛市",
district: "市北区",
street: "哈尔滨路",
streetNumber: "13号",
};
// 由Market Model创建一个instance
var market = new Market(marketJson);
var priceJson = {
date: new Date("2014-10-16"),
name: "大白菜",
_market: market._id, // 拿_market的id作为ref
min: 0.9,
max: 1.1,
avg: 1,
};
// instance直接save的方式添加document
market.save(function (err) {
if (err) return handleError(err);
console.log("create market success", market);
// 使用Model的create方法添加document
Price.create(priceJson, function (err, price) {
if (!err) {
console.log("create success", price);
emitter.emit("startQuery"); // 触发下一步的查找操作
}
});
});
}
query
增删改查这四个里面,查是永远的主角。MongoDB 不是关系型数据库,没有多表联合查询这一说,因此需要转换一下思维。
首先,在 MongoDB 里尽量不要拆表,能用一个 collection 表示的,就别再拆出一个 collection 来,这样做的好处是减少多表查询,提高性能。
如果实在不能忍了,比如就是有一个字段,不仅数据量大,而且重复特别多,那就拆表。由于 MongoDB 不支持多表联合查询,所以要通过写程序来实现。自己写程序,就要避开坑,比如
where A.a = ‘XXX’ and B.b = ‘YYY’
A 和 B 是不同的 collection,正确做法是把 A 和 B 中所有满足条件的先都取出来,再写代码求交集。可千万别在取交集的时候,每做一次比较都去 query 数据库,那样性能就完蛋了。
mongoose 提供了一个 populate 方法,可以简化联合查询的写法,即将外键字段直接填充成对象,而不仅仅是一个外键 id,请看下面例子:
// 查询示例代码
function query() {
// findOne查一个
var findPromise = Price.findOne({ name: "大白菜" }).exec();
// exec的返回值,同时继承了Promise和EventEmitter
// 因此既可以then也可以on
// 利用Promise的then方法来等待操作结果
findPromise.then(
function (price) {
console.log("promise fulfill");
console.log(price); // findOne返回单个对象
},
function (err) {
console.log("promise reject");
return handleError(err);
}
);
// 利用侦听complete/err事件来等待操作结果
findPromise
.on("complete", function (price) {
console.log("event complete");
console.log(price); // findOne返回单个对象
})
.on("err", function (err) {
handleError(err);
});
// 用populate填充外键字段
Price.findOne({ name: "大白菜" })
.populate("_market")
.exec(function (err, price) {
if (err) return handleError(err);
console.log("after populate >>>>>>>> \n", price);
});
// find查多个
Price.find({ name: "大白菜" }) // equal
.find({ name: /^大/ }) // like
.where("avg")
.gt(0.8)
.lt(1.2) // less than, greater than
.where("avg")
.gte(0.8)
.lte(1.2) // between
.where("name")
.in(["大白菜", "菠菜"]) // in
.sort("avg") // order by asc
.sort("-avg") // order by desc
.limit(10) // top
.select("name min max avg market") // select
.exec(function (err, prices) {
if (err) return handleError(err);
console.log(prices); // find返回数组
emitter.emit("startUpdate");
});
}
update
更新没啥好说的,会写 conditions 条件基本就没问题了。还有一种更新方法,是先查找出 instance,直接修改 instance 的属性,然后调用 intance 的 save 方法。
// update示例代码
function update() {
// Model.update(conditions, update, [options], [callback])
Price.update(
{ name: "大白菜", avg: { $gt: 0.9 } }, // condition
{ avg: 1.1 }, // update to
{ multi: true }, // options
function (err, numberAffected, raw) {
// callback
if (err) return handleError(err);
console.log("The number of updated documents was %d", numberAffected);
console.log("The raw response from Mongo was ", raw);
emitter.emit("startRemove");
}
);
}
remove
删除没啥好说的,注意一点,如果要立即执行删除,最简单的方法是传入 callback,否则只会返回一个 query 对象,并不立即执行。其实增删改查也都一样,想立即执行,要么传入 callback,要么调用 exec 方法。
// remove示例代码
function remove() {
// 删一个
Price.findOneAndRemove({ name: "不存在" }, function (err) {
// 删除不存在的不会报错
if (err) return handleError(err);
});
// 删多个
Price.remove({ name: "大白菜" }, function (err) {
if (err) return handleError(err);
});
// 全删
Market.remove(function (err) {
if (err) return handleError(err);
});
}
用事件串行化
增删改查都是异步返回的,为保证一个操作完成后再执行下一个,就要有一个机制来串行化。callback 里面再套 callback 是一种做法,但不够优雅。Promise 的 then 是一种方法, NodeJS 本身提供的 event 也是很好的选择。下面例子是使用 event 来串行化。
emitter.on("startQuery", function () {
query();
});
emitter.on("startUpdate", function () {
update();
});
emitter.on("startRemove", function () {
remove();
});
create(); // 从创建开始