进击的猫鼬 (1)

2014-10-28

猫鼬是一种看似温顺,但实际上很厉害的小动物,据说性情凶暴起来足以杀死一条眼镜蛇。在程序员的世界里,猫鼬竟然也占有重要一席。最近接触的两个开源项目,都以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(); // 从创建开始

完整示例代码

回主页
京ICP备14007233-1号