mongoose 是 nodeJS 提供连接 mongodb 的一个库,是 node.js 版本的 mongodb 数据库的驱动。
Mongoose 是 MongoDB 的 ODM(Object Document Mapper)。MongoDB 是文档型数据库(Document Database),不是关系型数据库(Relational Database)。而Mongoose可以将 MongonDB 数据库存储的文档(documents)转化为 javascript 对象,然后可以直接进行数据的增删改查。
mongoose 与 Oracle 关系型数据库、MongoDB 中基本概念的对比。
Oracle | MongoDB | Mongoose |
---|---|---|
数据库实例(database instance) | MongoDB实例 | Mongoose |
模式(schema) | 数据库(database) | mongoose |
表(table) | 集合(collection) | 模板(Schema)+模型(Model) |
行(row) | 文档(document) | 实例(instance) |
rowid | _id | _id |
Join | DBRef | DBRef |
mongoose 里面有三个基本概念:
- Schema:相当于是一个数据库的模板
- Model: 基本文档数据的父类,通过集成 Schema 定义的基本方法和属性得到相关的内容
- Instance:相当于是 document,通过 new Model() 初始化得到
三者的关系,先看下面的 demo:
'use strict';
const mongoose = require('mongoose');
const db = mongoose.connect('mongodb://localhost:27017/test', {config: {autoIndex: false}});
db.connection.on('error', error => {
console.log('数据库连接失败');
});
db.connection.on('open', error => {
console.log('数据库连接成功');
});
// 定义 schema
const PersonSchema = new mongoose.Schema({
name: String,
home: String,
age: { type: Number, default: 0 },
time: { type: Date, default: Date.now },
email: String
});
// 创建模型,用来操作数据库中的 person 集合, person 集合就是 mongodb 数据库中的 collection 集合
// 这里,我们一定要搞清楚一个东西. 实际上, mongoose.model里面定义的第一个参数,比如’person’, 并不是数据库中的, collection. 他只是collection的单数形式, 实际上在db中的collection是’persons’.
const PersonModel = db.model('person', PersonSchema);
// 根据模型创建实体对象,也就是生成一个 document
const Tom = new PersonModel({
name: 'Tom',
age: 15,
email: 'tom@gmail.com',
home: 'USA'
});
// 保存 document 到数据库中去
Tom.save(function(error, doc) {
if (error) {
console.log('error:' + error);
} else {
console.log(doc);
}
});
在 schema 中设置 timestamps 为 true, schema 映射的文档 document 会自动添加 createdAt 和 updatedAt 这两个字段,代表创建时间和更新时间。
每一个文档 document 都会被 mongoose 添加一个不重复的 _id , _id 的数据类型不是字符串,而是 ObjectID 类型。如果在查询语句中要使用 _id ,则需要使用 findById 语句,而不能使用 find 或 findOne 语句。
const UserSchema = new Schema(
{...},
{ timestamps: true }
);
这里设置索引分两种,一种设在 Schema filed, 另外一种设在 Schema.index 里。
// filed level 的索引
const animalSchema = new animalSchema({
name: String,
type: String,
tags: { type: [String], index: true }
});
// Sechema level 的索引
animalSchema.index({ name: 1, type: -1 });
// 1 表示正序, -1 表示逆序
实际上,两者效果是一样的. 看每个人的喜好了. 不过推荐直接在Schema level中设置, 这样分开能够增加可读性. 不过,官方给出了一个建议, 因为在创建字段时, 数据库会自动根据自动排序(createIndex). 有可能严重拖慢查询或者创建速度,所以一般而言,我们需要将该option 关闭。
//真心推荐
mongoose.connect('mongodb://user:pass@localhost:port/database', { config: { autoIndex: false } });
关于文档的创建,有三种方法, 一种是使用实例的 save() 方法,一种是使用 Model 的 create() 方法创建,以及使用 Model 的 insertMany() 方法。
- document.save()
- Model.create()
- Model.insertMany()
// ...
const Tank = mongoose.model('Tank', TankSchema);
const small = new Tank({size: 'small'});
// 使用实例来创建
small.save(function(err) {
if (err) return handleError(err);
// saved
});
// 使用 Model 的 create 方法创建
Tank.create({size: 'small'}, function(err, small) {
if (err) return handleError(err);
// saved
});
// 使用 Model 的 insertMany 方法创建
Tank.insertMany([{name: 'a'}, {name: 'b'}], function(err, small) {
if (err) return handleError(err);
// saved
});
Mongoose 查找文档有三种方法:
- find():查找所有文档
- findOne():查找某一个文档
- findById():通过 _id 来查找文档
Model.find(conditions, [projections], [options], [callback])
第一个参数表示查询条件,第二个参数用于控制返回的字段,第三个参数用于配置查询参数,第四个参数是回调函数,回调函数的形式为 function(err,docs){}。
// 没有指定查询条件的时候,查询当前集合中所有的文档
Modole.find(function(err, docs) {
console.log(docs);
});
//找出年龄大于18且名字里存在'huo'的数据
Model.find({{age: $gte: 18}, name: /huo/}, function(err, docs) {
console.log(docs);
})
// 只返回 name,同时还不返回 _id
Model.find({name:/a/},{name:1,_id:0},function(err,docs){
//例如:[ { name: 'huochai' }, { name: 'wang' } ]
console.log(docs);
})
// 找出跳过前两条数据的其他所有数据
Model.find({name:/a/}, null, {skip: 2}, function(err,docs){
//例如:[ { name: 'huochai' }, { name: 'wang' } ]
console.log(docs);
})
该方法返回查找到的所有实例的第一个。
首先得知道文档的 _id,然后将 _id 作为查询条件来查询文档
Model.find(_id, function(err, doc) {
console.log(doc);
})
文档查询中,常用的查询条件如下:
操作符 | 说明 |
---|---|
$or | 或关系 |
$nor | 或关系取反 |
$gt | 大于 |
$gte | 大于等于 |
$lt | 小于 |
$lte | 小于等于 |
$ne | 不等于 |
$in | 在多个值范围内 |
$nin | 不在多个值范围内 |
$all | 匹配数组中多个值 |
$regex | 正则,用于模糊查询 |
$size | 匹配数组大小 |
$maxDistance | 范围查询,距离(基于LBS) |
$mod | 取模运算 |
$near | 邻域查询,查询附近的位置(基于LBS) |
$exists | 字段是否存在 |
$elemMatch | 匹配内数组内的元素 |
$within | 范围查询(基于LBS) |
$box | 范围查询,矩形范围(基于LBS) |
$center | 范围醒询,圆形范围(基于LBS) |
$centerSphere | 范围查询,球形范围(基于LBS) |
$slice | 查询字段集合中的元素(比如从第几个之后,第N到第M个元素 |
如果要进行更复杂的查询,需要使用 $where 操作符, $where 操作符功能强大而且灵活,它可以使用任意的 JavaScript 作为查询的一部分,包含 JavaScript 表达式的字符串或者 JavaScript 函数。
// 使用字符串
Model.find({$where:"this.x == this.y"},function(err,docs){
//[ { _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 },
//{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 } ]
console.log(docs);
})
// 使用函数
Model.find({$where:function(){
return obj.x !== obj.y;
}},function(err,docs){
//[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
//{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 } ]
console.log(docs);
})
更新文档的方法有:
- updateOne():更新符合筛选条件的第一个文档
- updateMany():更新复合筛选条件的所有文档
- find() + save():查找相关文档后,做某些操作后再保存
Model.updateOne({age: {$gte: 20}}, {age: 40}, function(err, raw) {
console.log(raw);
})
Model.updateMany({age: {$gte: 20}}, {age: 40}, function(err, raw) {
console.log(raw);
})
注意:如果设置的查找条件,数据库里的数据并不满足,默认什么事都不发生。如果设置 options 里的 upsert 参数为 true ,若没有符合查询条件的文档, mongo 将会综合第一第二个参数向集合插入一个新的文档
如果需要更新的操作比较复杂,可以使用 find() + save() 方法来处理。
//比如找到年龄小于30岁的数据,名字后面添加 '30' 字符。
Model.find({age: {$gte: 20}}, function(err, docs) {
docs.forEach(function(item, index){
item.name += '30';
item.save();
})
})
- remove():删除所有满足筛选条件的文档
- findOneAndRemove():删除满足条件的第一个文档
remove() 方法有两种形式,一种是文档的 remove() 方法,一种是 Model 的 remove() 方法.
Model.remove({name: /a/}, function(err, docs) {
console.log(docs);
})
findOneAndRemove() 方法只删除掉符合条件的第一条数据。
Model.findOneAndRemove({name: /a/}, function(err, doc) {
console.log(doc);
})
前后钩子即 pre() 和 post() 方法,又称为中间件,是在执行某些操作时可以执行的函数。中间件在 schema 上指定,类似于静态方法或实例方法等。
pre 中间件是在方法执行前调用的。
var schema = new mongoose.Schema({ age:Number, name: String,x:Number,y:Number});
schema.pre('find',function(next){
console.log('我是pre方法1');
next();
});
schema.pre('find',function(next){
console.log('我是pre方法2');
next();
});
var temp = mongoose.model('temp', schema);
temp.find(function(err,docs){
console.log(docs[0]);
})
/*
我是pre方法1
我是pre方法2
{ _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 }
*/
post 中间件在方法执行之后调用,这个时候每个 pre 中间件都已经完成。post 方法可以传入 next 参数。
schema.post('init', function(doc, next) {
console.log('%s has been initialized from the db', doc._id);
});
schema.post('validate', function(doc, next) {
console.log('%s has been validated (but not saved yet)', doc._id);
});
schema.post('save', function(doc, next) {
console.log('%s has been saved', doc._id);
});
schema.post('remove', function(doc, next) {
console.log('%s has been removed', doc._id);
});
常用的查询后处理的方法如下所示:
关键字 | 说明 |
---|---|
sort | 排序 |
skip | 跳过 |
limit | 限制 |
select | 显示字段 |
exect | 执行 |
count | 计数 |
distinct | 去重 |
如果传入的参数是个对象,字段值可以是 asc, desc, ascending, descending, 1 和 -1。
如果传入参数是字符串,它得是以空格间隔的字段路径名列表。每个字段的排列顺序默认是正序,如果字段名有 - 前缀, 那么这个字段是倒序。
// 按照 "field" 字段正序、"test" 字段倒序排列
Model.find().sort({ field: 'asc', test: -1 });
// 等效于
Model.find().sort('field -test');
跳过指定的个数的文档
限制显示多少个文档
控制某些字段的显示或者不显示。 1 - 显示,0 - 不显示。
// 显示 name 字段和 age 字段,不显示 _id 字段
Model.find().select({name:1, age:1, _id:0}).exec(function(err,docs){
//[ { name: 'huochai', age: 27 },{ name: 'wang', age: 18 },{ name: 'huo', age: 30 },{ name: 'li', age: 20 } ]
console.log(docs);
});
显示集合中文档的个数,不过已经废弃了。一般是用 countDocuments
和 estimatedDocumentCount
。
// count()
Model.find().count(function(err, count) {
console.log(count);
})
// countDocuments(query)
// 用来查询特定条件的文档的个数
SomeModel.countDocuments({ name: 'Snow' }, function(err, count) {
//see other example
}
// 查询所有文档的个数
SomeModel.countDocuments({ }, function(err, count) {
//see other example
}
// estimatedDocumentCount() 只能用来查询所有的文档个数,没有 query 参数
SomeModel.estimatedDocumentCount().then(count => {
console.log(count);
}).catch(err => {
console.log(count);
});
用来筛选不重复的值或者字段
// Link 为 Model
Link.distinct('url', { clicks: {$gt: 100}}, function (err, result) {
if (err) return handleError(err);
assert(Array.isArray(result));
console.log('unique urls with more than 100 clicks', result);
})
Model.find().distinct('x',function(err,distinct){
console.log(distinct);//[ 1, 2 ]
});
因为 MongoDB 是文档型数据库,所以它没有关系型数据库 joins (数据库的两张表通过"外键",建立连接关系。) 特性。也就是在建立数据的关联时会比较麻烦。为了解决这个问题,Mongoose 封装了一个 Population 功能。使用 Population 可以实现在一个 document 中填充其他 collection(s) 的 document(s)。
在定义 Schema 的时候,如果设置某个 field 关联另一个 Schema ,那么在获取 document 的时候,就可以使用 population 功能通过关联 Schema 的 field 找到关联的另一个 document,并且用被关联 document 的内容替换掉原来关联字段(field)的内容。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: {type: String, unique: true},
posts: [{type: Schema.Types.ObjectId, ref: 'Post'}]
});
const User = mongoose.model('User', UserSchema);
const PostSchema = new Schema({
poster: {type: Schema.Types.type, ref: 'User'},
comments: [{type: Schema.Types.ObjectId, ref: 'Comment'}],
title: String,
content: String
});
const Post = mongoose.model('Post', PostSchema);
const CommentSchema = new Schema({
post: {type: Schema.Types.ObjectId, ref: 'Post'},
commenter: {type: Schema.Types.ObjectId, ref: 'User'},
content: String
});
const Comment = mongoose.model('Comment', CommentSchema);
在上面的例子中,创建了三个 Model: User, Post, Comment:
- User 的属性 posts,对应是一个 ObjectId 的数组,ref 表示关联 Post(此处的 Post 为
const Post = mongoose.model('Post', PostSchema)
的第一参数)。 - Post 的属性 poster 和 comments 分别关联 User 和 Comment。
- Comment 的属性 post 和 commenter 分别关联 Post 和 User。
- 三个 Model 的关系:一个 user--has many-->post。一个 post--has one-->user,has many-->comment。一个 comment--has one-->post 和 user。
// 连接数据库
mongoose.connect('mongodb://localhost/population-test', function (err){
if (err) throw err;
createData();
});
function createData() {
var userIds = [new ObjectId, new ObjectId, new ObjectId];
var postIds = [new ObjectId, new ObjectId, new ObjectId];
var commentIds = [new ObjectId, new ObjectId, new ObjectId];
var users = [];
var posts = [];
var comments = [];
users.push({
_id : userIds[0],
name : 'aikin',
posts : [postIds[0]]
});
users.push({
_id : userIds[1],
name : 'luna',
posts : [postIds[1]]
});
users.push({
_id : userIds[2],
name : 'luajin',
posts : [postIds[2]]
});
posts.push({
_id : postIds[0],
title : 'post-by-aikin',
poster : userIds[0],
comments : [commentIds[0]]
});
posts.push({
_id : postIds[1],
title : 'post-by-luna',
poster : userIds[1],
comments : [commentIds[1]]
});
posts.push({
_id : postIds[2],
title : 'post-by-luajin',
poster : userIds[2],
comments : [commentIds[2]]
});
comments.push({
_id : commentIds[0],
content : 'comment-by-luna',
commenter : userIds[1],
post : postIds[0]
});
comments.push({
_id : commentIds[1],
content : 'comment-by-luajin',
commenter : userIds[2],
post : postIds[1]
});
comments.push({
_id : commentIds[2],
content : 'comment-by-aikin',
commenter : userIds[1],
post : postIds[2]
});
User.create(users, function(err, docs) {
Post.create(posts, function(err, docs) {
Comment.create(comments, function(err, docs) {
});
});
});
}
语法:
Query.populate(path, [select], [model], [match], [options])
参数:
-
path: 类型为 String 或者 Object。String类型的时, 指定要填充的关联字段,要填充多个关联字段可以以空格分隔。Object类型的时,就是把 populate 的参数封装到一个对象里。
-
select:类型:Object 或 String ,可选,指定填充 document 中的哪些字段。Object 类型的时,格式如: {name: 1, _id: 0},为0表示不填充,为1时表示填充。String 类型的时,格式如: "name -_id" ,用空格分隔字段,在字段名前加上 - 表示不填充。
-
model:类型:Model,可选,指定关联字段的 model ,如果没有指定就会使用 Schema 的 ref。
-
match:类型:Object,可选,指定附加的查询条件。
-
options:类型:Object,可选,指定附加的其他查询选项,如排序以及条数限制等等。
User.find().populate({
path: 'posts',
select: {title: 1},
options: {
sort: {title: -1}
}
}).exec(function(err, docs) {
console.log(docs[0].posts[0].title);
// post-by-aikin
})
Post.findOne({title: 'post-by-aikin'})
.populate({
path: 'poster comments',
select: {_id: 0}
})
.exec(function(err, doc) {
console.log(doc.poster.name); // aikin
console.log(doc.poster._id); // undefined
console.log(doc.comments[0].content); // comment-by-luna
console.log(doc.comments[0]._id); // undefined
});
语法:
Model.populate(docs, options, [cb(err,doc)])
填充Post的poster和comments字段以及comments的commenter字段:
Post.find({title: 'post-by-aikin'})
.populate('poster comments')
.exec(function(err, docs) {
var opts = [{
path : 'comments.commenter',
select : 'name',
model : 'User'
}];
Post.populate(docs, opts, function(err, populatedDocs) {
console.log(populatedDocs[0].poster.name); // aikin
console.log(populatedDocs[0].comments[0].commenter.name); // luna
});
});
语法:
Document.populate([path], [callback])
填充User的posts字段:
User.findOne({name: 'aikin'})
.exec(function(err, doc) {
var opts = [{
path : 'posts',
select : 'title'
}];
doc.populate(opts, function(err, populatedDoc) {
console.log(populatedDoc.posts[0].title); // post-by-aikin
});
});