Skip to content

Latest commit

 

History

History
670 lines (519 loc) · 19 KB

6.node.js驱动-mongoose.md

File metadata and controls

670 lines (519 loc) · 19 KB

概念

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 } });  

CRUD 操作

创建

关于文档的创建,有三种方法, 一种是使用实例的 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 来查找文档

find()

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);
})

findOne()

该方法返回查找到的所有实例的第一个。

findById()

首先得知道文档的 _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():查找相关文档后,做某些操作后再保存

updateOne()

Model.updateOne({age: {$gte: 20}}, {age: 40}, function(err, raw) {
    console.log(raw);
})

updateMany()

Model.updateMany({age: {$gte: 20}}, {age: 40}, function(err, raw) {
    console.log(raw);
})

注意:如果设置的查找条件,数据库里的数据并不满足,默认什么事都不发生。如果设置 options 里的 upsert 参数为 true ,若没有符合查询条件的文档, mongo 将会综合第一第二个参数向集合插入一个新的文档

find() + save()

如果需要更新的操作比较复杂,可以使用 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() 方法有两种形式,一种是文档的 remove() 方法,一种是 Model 的 remove() 方法.

Model.remove({name: /a/}, function(err, docs) {
    console.log(docs);
})

findOneAndRemove()

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 去重

sort

如果传入的参数是个对象,字段值可以是 asc, desc, ascending, descending, 1 和 -1。

如果传入参数是字符串,它得是以空格间隔的字段路径名列表。每个字段的排列顺序默认是正序,如果字段名有 - 前缀, 那么这个字段是倒序。

// 按照 "field" 字段正序、"test" 字段倒序排列
Model.find().sort({ field: 'asc', test: -1 });

// 等效于
Model.find().sort('field -test');

skip

跳过指定的个数的文档

limit

限制显示多少个文档

select

控制某些字段的显示或者不显示。 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);
}); 

count

显示集合中文档的个数,不过已经废弃了。一般是用 countDocumentsestimatedDocumentCount

// 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);
});

distinct

用来筛选不重复的值或者字段

// 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 ]
});

集合的关联 populate

因为 MongoDB 是文档型数据库,所以它没有关系型数据库 joins (数据库的两张表通过"外键",建立连接关系。) 特性。也就是在建立数据的关联时会比较麻烦。为了解决这个问题,Mongoose 封装了一个 Population 功能。使用 Population 可以实现在一个 document 中填充其他 collection(s) 的 document(s)。

在定义 Schema 的时候,如果设置某个 field 关联另一个 Schema ,那么在获取 document 的时候,就可以使用 population 功能通过关联 Schema 的 field 找到关联的另一个 document,并且用被关联 document 的内容替换掉原来关联字段(field)的内容。

先建立三个 Schema 和 Model:

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()

语法:

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 的 posts 字段:

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的poster和comments字段:

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()

语法:

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()

语法:

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
        });
    });