Mongoose基礎入門


前面的話

  Mongoose是在node.js異步環境下對mongodb進行便捷操作的對象模型工具。本文將詳細介紹如何使用Mongoose來操作MongoDB

 

NodeJS驅動

  在介紹Mongoose之前,首先介紹使用NodeJS操作MongoDB的方法

  如果使用程序操作數據庫,就要使用MongoDB驅動。MongoDB驅動實際上就是為應用程序提供的一個接口,不同的語言對應不同的驅動,NodeJS驅動不能應用在其他后端語言中

  首先,安裝mongodb

npm install mongodb

  接着,使用require()方法引入mongodb數據庫;然后使用MongoClient對象的connect()方法連接mongodb;最后通過node來對mongodb進行異步的增刪改查

  在mongodb數據庫中建立db1數據庫,然后通過以下代碼,建立col集合,並插入{"a":1}文檔

var mongodb = require('mongodb');
mongodb.MongoClient.connect("mongodb://localhost/db1",function(err,db){
    if(!err){
        db.collection("col").insert({"a":1},function(err,result){
            if(!err){
                console.log(result);
            }
        })
    }
})

  最后返回結果如下

{ result: { ok: 1, n: 1 },
  ops: [ { a: 1, _id: 597077dc271d092728caa362 } ],
  insertedCount: 1,
  insertedIds: [ 597077dc271d092728caa362 ] }

 

概述

  Mongoose是NodeJS的驅動,不能作為其他語言的驅動。Mongoose有兩個特點

  1、通過關系型數據庫的思想來設計非關系型數據庫

  2、基於mongodb驅動,簡化操作

  Mongooose中,有三個比較重要的概念,分別是Schema、Model、Entity。它們的關系是:Schema生成Model,Model創造Document,Model和Document都可對數據庫操作造成影響,但Model比Document更具操作性

  Schema用於定義數據庫的結構。類似創建表時的數據定義(不僅僅可以定義文檔的結構和屬性,還可以定義文檔的實例方法、靜態模型方法、復合索引等),每個Schema會映射到mongodb中的一個collection,Schema不具備操作數據庫的能力

  Model是由Schema編譯而成的構造器,具有抽象屬性和行為,可以對數據庫進行增刪查改。Model的每一個實例(instance)就是一個文檔document

  Document是由Model創建的實體,它的操作也會影響數據庫

 

安裝

  安裝nodejsmongodb之后 ,使用npm來安裝mongoose

npm install mongoose

  安裝成功后,就可以通過 require('mongoose') 來使用

 

連接數據庫

  使用require()方法在項目中包含mongoose后,接下來使用connect()方法連接到MongoDB數據庫

【connect()】

mongoose.connect(url);

  connect()最簡單的使用方式,就是只要傳入url參數即可,如下所示。連接到本地localhost的db1服務器

mongoose.connect('mongodb://localhost/db1');

  如果還需要傳遞用戶名、密碼,則可以使用如下方式

mongoose.connect('mongodb://username:password@host:port/database?options...');

  connect()方法還接受一個選項對象options,該對象將傳遞給底層驅動程序。這里所包含的所有選項優先於連接字符串中傳遞的選項

mongoose.connect(uri, options);

  可用選項如下所示

 db            -數據庫設置
 server        -服務器設置
 replset       -副本集設置
 user          -用戶名
 pass          -密碼
 auth          -鑒權選項
 mongos        -連接多個數據庫
 promiseLibrary
var options = {
  db: { native_parser: true },
  server: { poolSize: 5 },
  replset: { rs_name: 'myReplicaSetName' },
  user: 'myUserName',
  pass: 'myPassword'
}
mongoose.connect(uri, options);

  如果要連接多個數據庫,只需要設置多個url以,隔開,同時設置mongos為true

mongoose.connect('urlA,urlB,...', {
   mongos : true 
})

  connect()函數還接受一個回調參數

mongoose.connect(uri, options, function(error) {

});

   執行下列代碼后,控制台輸出“連接成功”

var mongoose = require('mongoose');
mongoose.connect("mongodb://localhost/test", function(err) {
    if(err){
        console.log('連接失敗');
    }else{
        console.log('連接成功');
    }
});

   如果開啟鑒權控制,以用戶名"u1",密碼"123456"登錄'db1'數據庫。執行代碼后,控制台輸出“連接成功”

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(err){
        console.log('連接失敗');
    }else{
        console.log('連接成功');
    }
});

【disconnect()】 

mongoose.disconnect()

   使用disconnect()方法可以斷開連接

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(err){
        console.log('連接失敗');
    }else{
        console.log('連接成功');
    }
});
setTimeout(function(){
    mongoose.disconnect(function(){
        console.log("斷開連接");
    })
}, 2000);

 

Schema

  Schema主要用於定義MongoDB中集合Collection里文檔document的結構  

  定義Schema非常簡單,指定字段名和類型即可,支持的類型包括以下8種

String      字符串
Number      數字    
Date        日期
Buffer      二進制
Boolean     布爾值
Mixed       混合類型
ObjectId    對象ID    
Array       數組

  通過mongoose.Schema來調用Schema,然后使用new方法來創建schema對象

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var mySchema = new Schema({
  title:  String,
  author: String,
  body:   String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs:  Number
  }
});

  [注意]創建Schema對象時,聲明字段類型有兩種方法,一種是首字母大寫的字段類型,另一種是引號包含的小寫字段類型

var mySchema = new Schema({title:String, author:String});
//或者 
var mySchema = new Schema({title:'string', author:'string'});

  如果需要在Schema定義后添加其他字段,可以使用add()方法

var MySchema = new Schema;
MySchema.add({ name: 'string', color: 'string', price: 'number' });

【timestamps】

  在schema中設置timestamps為true,schema映射的文檔document會自動添加createdAt和updatedAt這兩個字段,代表創建時間和更新時間

var UserSchema = new Schema(
  {...},
  { timestamps: true }
);

【_id】

  每一個文檔document都會被mongoose添加一個不重復的_id,_id的數據類型不是字符串,而是ObjectID類型。如果在查詢語句中要使用_id,則需要使用findById語句,而不能使用find或findOne語句

  

Model

  模型Model是根據Schema編譯出的構造器,或者稱為類,通過Model可以實例化出文檔對象document

  文檔document的創建和檢索都需要通過模型Model來處理

【model()】

mongoose.model()

  使用model()方法,將Schema編譯為Model。model()方法的第一個參數是模型名稱

  [注意]一定要將model()方法的第一個參數和其返回值設置為相同的值,否則會出現不可預知的結果

  Mongoose會將集合名稱設置為模型名稱的小寫版。如果名稱的最后一個字符是字母,則會變成復數;如果名稱的最后一個字符是數字,則不變;如果模型名稱為"MyModel",則集合名稱為"mymodels";如果模型名稱為"Model1",則集合名稱為"model1"

var schema = new mongoose.Schema({ num:Number, name: String, size: String});
var MyModel = mongoose.model('MyModel', schema);

【實例化文檔document】

  通過對原型Model1使用new方法,實例化出文檔document對象

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(err){
        console.log('連接失敗');
    }else{
        console.log('連接成功');
        var schema = new mongoose.Schema({ num:Number, name: String, size: String});
        var MyModel = mongoose.model('MyModel', schema);
        var doc1 = new MyModel({ size: 'small' });
        console.log(doc1.size);//'small'
    }
});

【文檔保存】

  通過new Model1()創建的文檔doc1,必須通過save()方法,才能將創建的文檔保存到數據庫的集合中,集合名稱為模型名稱的小寫復數版

  回調函數是可選項,第一個參數為err,第二個參數為保存的文檔對象

save(function (err, doc) {})
var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ num:Number, name: String, size: String });
        var MyModel = mongoose.model('MyModel', schema);
        var doc1 = new MyModel({ size: 'small' });
        doc1.save(function (err,doc) {
        //{ __v: 0, size: 'small', _id: 5970daba61162662b45a24a1 }
          console.log(doc);
        })
    }
});

  由下圖所示,db1數據庫中的集合名稱為mymodels,里面有一個{size:"small"}的文檔

 

自定義方法

【實例方法】

  Model的實例是document,內置實例方法有很多,如 save,可以通過Schema對象的methods屬性給實例自定義擴展方法

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ num:Number, name: String, size: String });        
        schema.methods.findSimilarSizes = function(cb){
            return this.model('MyModel').find({size:this.size},cb);
        }
        var MyModel = mongoose.model('MyModel', schema);
        var doc1 = new MyModel({ name:'doc1', size: 'small' });
        var doc2 = new MyModel({ name:'doc2', size: 'small' });
        var doc3 = new MyModel({ name:'doc3', size: 'big' });
        doc1.save();
        doc2.save();
        doc3.save();
        setTimeout(function(){
            doc1.findSimilarSizes(function(err,docs){
                docs.forEach(function(item,index,arr){
                    //doc1
                    //doc2
                     console.log(item.name)        
                })
            })  
        },0)  
    }
});

【靜態方法】

  通過Schema對象的statics屬性給 Model 添加靜態方法

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ num:Number, name: String, size: String });        
        schema.statics.findByName = function(name,cb){
            return this.find({name: new RegExp(name,'i')},cb);
        }
        var MyModel = mongoose.model('MyModel', schema);
        var doc1 = new MyModel({ name:'doc1', size: 'small' });
        var doc2 = new MyModel({ name:'doc2', size: 'small' });
        var doc3 = new MyModel({ name:'doc3', size: 'big' });
        doc1.save();
        doc2.save();
        doc3.save();
        setTimeout(function(){
            MyModel.findByName('doc1',function(err,docs){
                //[ { _id: 5971e68f4f4216605880dca2,name: 'doc1',size: 'small',__v: 0 } ]
                console.log(docs);
            })  
        },0)  
    }
});

  由上所示,實例方法和靜態方法的區別在於,靜態方法是通過Schema對象的statics屬性model添加方法,實例方法是通過Schema對象的methods是給document添加方法

【查詢方法】

  通過schema對象的query屬性,給model添加查詢方法

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        schema.query.byName = function(name){
            return this.find({name: new RegExp(name)});
        }
        var temp = mongoose.model('temp', schema);   
        temp.find().byName('huo').exec(function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
            // { _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 } ]
            console.log(docs);
        })  

    }           
});

 

文檔新增

  文檔新增有三種方法,一種是使用上面介紹過的文檔的save()方法,另一種是使用模型model的create()方法,最后一種是模型model的insertMany()方法

【save()】

  [注意]回調函數可以省略

save([options], [options.safe], [options.validateBeforeSave], [fn])

  新建{age:10,name:'save'}文檔,並保存

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);
        //使用鏈式寫法    
        new temp({age:10,name:'save'}).save(function(err,doc){
            //[ { _id: 59720bc0d2b1125cbcd60b3f, age: 10, name: 'save', __v: 0 } ]
            console.log(doc);        
        });         
    }           
});

【create()】

  使用save()方法,需要先實例化為文檔,再使用save()方法保存文檔。而create()方法,則直接在模型Model上操作,並且可以同時新增多個文檔

Model.create(doc(s), [callback])

  新增{name:"xiaowang"},{name:"xiaoli"}這兩個文檔

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);   
        temp.create({name:"xiaowang"},{name:"xiaoli"},function(err,doc1,doc2){
            //{ __v: 0, name: 'xiaowang', _id: 59720d83ad8a953f5cd04664 }
            console.log(doc1); 
            //{ __v: 0, name: 'xiaoli', _id: 59720d83ad8a953f5cd04665 }
            console.log(doc2); 
        });       
    }           
});

【insertMany()】

Model.insertMany(doc(s), [options], [callback])

  新增{name:"a"},{name:"b"}這兩個文檔

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);   
        temp.insertMany([{name:"a"},{name:"b"}],function(err,docs){
            //[ { __v: 0, name: 'a', _id: 59720ea1bbf5792af824b30c },
            //{ __v: 0, name: 'b', _id: 59720ea1bbf5792af824b30d } ]
            console.log(docs); 
        });       

    }           
});

  

文檔查詢

  使用Mongoose來查找文檔很容易,有以下3種方法可供選擇

find()
findById()
findOne()

【find()】

  第一個參數表示查詢條件,第二個參數用於控制返回的字段,第三個參數用於配置查詢參數,第四個參數是回調函數,回調函數的形式為function(err,docs){}

Model.find(conditions, [projection], [options], [callback])

  在數據庫db1的集合temps中存在如下數據

  現在,使用find()方法找出所有數據

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);
        temp.find(function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
            //{ _id: 5971f93be6f98ec60e3dc86d, name: 'wang', age: 18 },
            //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 },
            //{ _id: 5971f93be6f98ec60e3dc86f, name: 'li', age: 12 } ]
            console.log(docs);
        })
    }
});

  找出年齡大於18的數據

        temp.find({age:{$gte:18}},function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
            //{ _id: 5971f93be6f98ec60e3dc86d, name: 'wang', age: 18 },
            //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 }]
            console.log(docs);
        })

  找出年齡大於18且名字里存在'huo'的數據

        temp.find({name:/huo/,age:{$gte:18}},function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
            //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 }]
            console.log(docs);
        })

  找出名字里存在'a'的數據,且只輸出'name'字段

  [注意]_id字段默認輸出

        temp.find({name:/a/},'name',function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai' },
            //{ _id: 5971f93be6f98ec60e3dc86d, name: 'wang' } ]
            console.log(docs);
        })

  如果確實不需要_id字段輸出,可以進行如下設置

        temp.find({name:/a/},{name:1,_id:0},function(err,docs){
            //[ { name: 'huochai' }, { name: 'wang' } ]
            console.log(docs);
        })

  找出跳過前兩條數據的其他所有數據

  [注意]如果使用第三個參數,前兩個參數如果沒有值,需要設置為null

        temp.find(null,null,{skip:2},function(err,docs){
            //[ { _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 },
            //{ _id: 5971f93be6f98ec60e3dc86f, name: 'li', age: 12 } ]
            console.log(docs);
        })

【findById()】

Model.findById(id, [projection], [options], [callback])

  顯示第0個元素的所有字段

        var aIDArr = [];
        temp.find(function(err,docs){
            docs.forEach(function(item,index,arr){
                aIDArr.push(item._id);
            })
            temp.findById(aIDArr[0],function(err,doc){
                //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 }
                console.log(doc);
            })            
        })

  以上代碼的另一種寫法如下

        var aIDArr = [];
        temp.find(function(err,docs){
            docs.forEach(function(item,index,arr){
                aIDArr.push(item._id);
            })
            temp.findById(aIDArr[0]).exec(function(err,doc){
                //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 }
                console.log(doc);
            })            
        })

  只輸出name字段

            temp.findById(aIDArr[0],{name:1,_id:0},function(err,doc){
                //{  name: 'huochai'}
                console.log(doc);
            })            

  或者寫成下面這種形式

            temp.findById(aIDArr[0],{name:1,_id:0}).exec(function(err,doc){
                //{  name: 'huochai'}
                console.log(doc);
            })            

  輸出最少的字段

            temp.findById(aIDArr[0],{lean:true},function(err,doc){
                //{ _id: 5971f93be6f98ec60e3dc86c }
                console.log(doc);
            })   
            temp.findById(aIDArr[0],{lean:true}).exec(function(err,doc){
                //{ _id: 5971f93be6f98ec60e3dc86c }
                console.log(doc);
            })     

【findOne()】

  該方法返回查找到的所有實例的第一個

Model.findOne([conditions], [projection], [options], [callback])

  找出age>20的文檔中的第一個文檔

temp.findOne({age:{$gt : 20}},function(err,doc){
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 }
    console.log(doc);
})   
temp.findOne({age:{$gt : 20}}).exec(function(err,doc){
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 }
    console.log(doc);
})  

  找出age>20的文檔中的第一個文檔,且只輸出name字段

temp.findOne({age:{$gt : 20}},{name:1,_id:0},function(err,doc){
    //{ name: 'huochai' }
    console.log(doc);
})   
temp.findOne({age:{$gt : 20}},{name:1,_id:0}).exec(function(err,doc){
    //{ name: 'huochai' }
    console.log(doc);
})     

  找出age>20的文檔中的第一個文檔,且輸出包含name字段在內的最短字段

temp.findOne({age:{$gt : 20}},"name",{lean:true},function(err,doc){
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai' }
    console.log(doc);
})   
temp.findOne({age:{$gt : 20}},"name").lean().exec(function(err,doc){
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai' }
    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操作符,$where操作符功能強大而且靈活,它可以使用任意的JavaScript作為查詢的一部分,包含JavaScript表達式的字符串或者JavaScript函數

  使用字符串

temp.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);
}) 
temp.find({$where:"obj.x == obj.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);
}) 

  使用函數

temp.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);
}) 
temp.find({$where:function(){
        return this.x !== this.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);
}) 

 

文檔更新

  文檔更新可以使用以下幾種方法

update()
updateMany()
find() + save() updateOne()
findOne() + save() findByIdAndUpdate() fingOneAndUpdate()

 【update()】

  第一個參數conditions為查詢條件,第二個參數doc為需要修改的數據,第三個參數options為控制選項,第四個參數是回調函數

Model.update(conditions, doc, [options], [callback])

  options有如下選項

   safe (boolean): 默認為true。安全模式。
  upsert (boolean): 默認為false。如果不存在則創建新記錄。
  multi (boolean): 默認為false。是否更新多個查詢記錄。
  runValidators: 如果值為true,執行Validation驗證。
  setDefaultsOnInsert: 如果upsert選項為true,在新建時插入文檔定義的默認值。
  strict (boolean): 以strict模式進行更新。
  overwrite (boolean): 默認為false。禁用update-only模式,允許覆蓋記錄。

  數據庫temps中現有數據如下

  現在使用update()方法查詢age大於20的數據,並將其年齡更改為40歲

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);   
        temp.update({age:{$gte:20}},{age:40},function(err,raw){
            //{ n: 1, nModified: 1, ok: 1 }
            console.log(raw);
        })

    }           
});

  經過以上操作,數據庫結果如下。只有第一個數據更改為40歲。而第三個數據沒有發生變化

  如果要同時更新多個記錄,需要設置options里的multi為true。下面將名字中有'a'字符的年齡設置為10歲

var mongoose = require('mongoose');
mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
    if(!err){
        var schema = new mongoose.Schema({ age:Number, name: String});        
        var temp = mongoose.model('temp', schema);   
        temp.update({name:/a/},{age: 10},{multi:true},function(err,raw){
            //{ n: 2, nModified: 2, ok: 1 }
            console.log(raw);
        })

    }           
});

  如果設置的查找條件,數據庫里的數據並不滿足,默認什么事都不發生

temp.update({age:100},{name: "hundred"},function(err,raw){
    //{ n: 0, nModified: 0, ok: 1 }
    console.log(raw);
})

  如果設置options里的upsert參數為true,若沒有符合查詢條件的文檔,mongo將會綜合第一第二個參數向集合插入一個新的文檔

temp.update({age:100},{name: "hundred"},{upsert:true},function(err,raw){
    //{ n: 1, nModified: 0,upserted: [ { index: 0, _id: 5972c202d46b621fca7fc8c7 } ], ok: 1 }
    console.log(raw);
})
temp.update({name:/aa/},{age: 0},{upsert:true},function(err,raw){
    //{ n: 1, nModified: 0,upserted: [ { index: 0, _id: 5972c288d46b621fca7fdd8f } ], ok: 1 }
    console.log(raw);
})

  [注意]update()方法中的回調函數不能省略,否則數據不會被更新。如果回調函數里並沒有什么有用的信息,則可以使用exec()簡化代碼

temp.update({name:/aa/},{age: 0},{upsert:true}).exec();

【updateMany()】

  updateMany()與update()方法唯一的區別就是默認更新多個文檔,即使設置{multi:false}也無法只更新第一個文檔

Model.updateMany(conditions, doc, [options], [callback])

  將數據庫中名字中帶有'huo'的數據,年齡變為50歲

temp.updateMany({name:/huo/},{age:50},function(err,raw){
    //{ n: 2, nModified: 2, ok: 1 }
    console.log(raw);
});

【find() + save()】

  如果需要更新的操作比較復雜,可以使用find()+save()方法來處理,比如找到年齡小於30歲的數據,名字后面添加'30'字符

temp.find({age:{$lt:20}},function(err,docs){
    //[ { _id: 5971f93be6f98ec60e3dc86d, name: 'wang', age: 10 },
    //{ _id: 5971f93be6f98ec60e3dc86f, name: 'li', age: 12 }]
    console.log(docs);
    docs.forEach(function(item,index,arr){
        item.name += '30';
        item.save();
    })
    //[ { _id: 5971f93be6f98ec60e3dc86d, name: 'wang30', age: 10 },
    // { _id: 5971f93be6f98ec60e3dc86f, name: 'li30', age: 12 }]
    console.log(docs);
});

【updateOne()】

   updateOne()方法只能更新找到的第一條數據,即使設置{multi:true}也無法同時更新多個文檔

  將數據庫中名字中帶有'huo'的數據,年齡變為60歲

temp.updateOne({name:/huo/},{age:60},function(err,raw){
    //{ n: 1, nModified: 1, ok: 1 }
    console.log(raw);
});

【findOne() + save()】

  如果需要更新的操作比較復雜,可以使用findOne()+save()方法來處理,比如找到名字為'huochai'的數據,年齡加100歲

temp.findOne({name:'huochai'},function(err,doc){
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 10 }
    console.log(doc);
    doc.age += 100;
    doc.save();
    //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 110 }
    console.log(doc);
});

【findOneAndUpdate()】

  fineOneAndUpdate()方法的第四個參數回調函數的形式如下function(err,doc){}

Model.findOneAndUpdate([conditions], [update], [options], [callback])

【findByIdAndUpdate】

   fineByIdAndUpdate()方法的第四個參數回調函數的形式如下function(err,doc){}

Model.findOneAndUpdate([conditions], [update], [options], [callback])

 

文檔刪除

  有三種方法用於文檔刪除

remove()
findOneAndRemove()
findByIdAndRemove()

【remove()】

  remove有兩種形式,一種是文檔的remove()方法,一種是Model的remove()方法

  下面介紹Model的remove()方法,該方法的第一個參數conditions為查詢條件,第二個參數回調函數的形式如下function(err){}  

model.remove(conditions, [callback])

  刪除數據庫中名稱包括'30'的數據

temp.remove({name:/30/},function(err){})

  [注意]remove()方法中的回調函數不能省略,否則數據不會被刪除。當然,可以使用exec()方法來簡寫代碼

temp.remove({name:/30/}).exec()

  下面介紹文檔的remove()方法,該方法的參數回調函數的形式如下function(err,doc){}

document.remove([callback])

  刪除數據庫中名稱包含'huo'的數據

  [注意]文檔的remove()方法的回調函數參數可以省略

temp.find({name:/huo/},function(err,doc){
    doc.forEach(function(item,index,arr){
        item.remove(function(err,doc){
            //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 30 }
            //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 60 }
            console.log(doc);
        })
    })
})  

【findOneAndRemove()】

  model的remove()會刪除符合條件的所有數據,如果只刪除符合條件的第一條數據,則可以使用model的findOneAndRemove()方法

Model.findOneAndRemove(conditions, [options], [callback])

   集合temps現有數據如下

  現在刪除第一個年齡小於20的數據

temp.findOneAndRemove({age:{$lt:20}},function(err,doc){
    //{ _id: 5972d3f3e6f98ec60e3dc873, name: 'wang', age: 18 }
    console.log(doc);
})

  與model的remove()方法相同,回調函數不能省略,否則數據不會被刪除。當然,可以使用exec()方法來簡寫代碼

temp.findOneAndRemove({age:{$lt:20}}).exec()

【findByIdAndRemove()】

Model.findByIdAndRemove(id, [options], [callback])

  刪除第0個元素

var aIDArr = [];
temp.find(function(err,docs){
    docs.forEach(function(item,index,arr){
        aIDArr.push(item._id);
    })
    temp.findByIdAndRemove(aIDArr[0],function(err,doc){
        //{ _id: 5972d754e6f98ec60e3dc882, name: 'huochai', age: 27 }
        console.log(doc);
    })            
})

  類似的,該方法也不能省略回調函數,否則數據不會被刪除。當然,可以使用exec()方法來簡寫代碼

var aIDArr = [];
temp.find(function(err,docs){
    docs.forEach(function(item,index,arr){
        aIDArr.push(item._id);
    })
    temp.findByIdAndRemove(aIDArr[0]).exec()            
})

 

前后鈎子

  前后鈎子即pre()和post()方法,又稱為中間件,是在執行某些操作時可以執行的函數。中間件在schema上指定,類似於靜態方法或實例方法等

  可以在數據庫執行下列操作時,設置前后鈎子

    init
    validate
    save
    remove
    count
    find
    findOne
    findOneAndRemove
    findOneAndUpdate
    insertMany
    update

【pre()】

  以find()方法為例,在執行find()方法之前,執行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()】

  post()方法並不是在執行某些操作后再去執行的方法,而在執行某些操作前最后執行的方法,post()方法里不可以使用next()

var schema = new mongoose.Schema({ age:Number, name: String,x:Number,y:Number});  
schema.post('find',function(docs){
    console.log('我是post方法1');
});
schema.post('find',function(docs){
    console.log('我是post方法2');
});
var temp = mongoose.model('temp', schema);
temp.find(function(err,docs){
    console.log(docs[0]);
}) 
/*
我是post方法1
我是post方法2
{ _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 }
 */   

 

查詢后處理

  常用的查詢后處理的方法如下所示

sort     排序
skip 跳過 limit 限制 select 顯示字段 exect 執行
count 計數
distinct 去重
var schema = new mongoose.Schema({ age:Number, name: String,x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
temp.find(function(err,docs){
    //[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 } ]
    console.log(docs);
}) 

【sort()】

  按age從小到大排序

temp.find().sort("age").exec(function(err,docs){
    //[ { _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 } ]
    console.log(docs);
}); 

  按x從小到大,age從大到小排列

temp.find().sort("x -age").exec(function(err,docs){
    //[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{  _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 } ]
    console.log(docs);
}); 

【skip()】

  跳過1個,顯示其他

temp.find().skip(1).exec(function(err,docs){
    //[ { _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 },
    //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 } ]
    console.log(docs);
}); 

【limit()】

  顯示2個

temp.find().limit(2).exec(function(err,docs){
    //[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1 } ]
    console.log(docs);
}); 

【select()】

  顯示name、age字段,不顯示_id字段

temp.find().select("name age -_id").exec(function(err,docs){
    //[ { name: 'huochai', age: 27 },{ name: 'wang', age: 18 },{ name: 'huo', age: 30 },{ name: 'li', age: 20 } ]
    console.log(docs);
}); 
temp.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);
}); 

  下面將以上方法結合起來使用,跳過第1個后,只顯示2個數據,按照age由大到小排序,且不顯示_id字段

temp.find().skip(1).limit(2).sort("-age").select("-_id").exec(function(err,docs){
    //[ { name: 'huochai', age: 27, x: 1, y: 2 },
    //{ name: 'li', age: 20, x: 2, y: 2 } ]
    console.log(docs);
}); 

【count()】

  顯示集合temps中的文檔數量

temp.find().count(function(err,count){
    console.log(count);//4
}); 

【distinct()】

  返回集合temps中的x的值

temp.find().distinct('x',function(err,distinct){
    console.log(distinct);//[ 1, 2 ]
}); 

 

文檔驗證

  為什么需要文檔驗證呢?以一個例子作為說明,schema進行如下定義

var schema = new mongoose.Schema({ age:Number, name: String,x:Number,y:Number});  

  如果不進行文檔驗證,保存文檔時,就可以不按照Schema設置的字段進行設置,分為以下幾種情況

  1、缺少字段的文檔可以保存成功

var temp = mongoose.model('temp', schema);
new temp({age:10}).save(function(err,doc){
    //{ __v: 0, age: 10, _id: 597304442b70086a1ce3cf05 }
    console.log(doc);
}); 

  2、包含未設置的字段的文檔也可以保存成功,未設置的字段不被保存

new temp({age:100,abc:"abc"}).save(function(err,doc){
    //{ __v: 0, age: 100, _id: 5973046a2bb57565b474f48b }
    console.log(doc);
}); 

  3、包含字段類型與設置不同的字段的文檔也可以保存成功,不同字段類型的字段被保存為設置的字段類型

new temp({age:true,name:10}).save(function(err,doc){
    //{ __v: 0, age: 1, name: '10', _id: 597304f7a926033060255366 }
    console.log(doc);
}); 

  而通過文檔驗證,就可以避免以下幾種情況發生

  文檔驗證在SchemaType中定義,格式如下

{name: {type:String, validator:value}}

  常用驗證包括以下幾種

required: 數據必須填寫
default: 默認值
validate: 自定義匹配
min: 最小值(只適用於數字)
max: 最大值(只適用於數字)
match: 正則匹配(只適用於字符串)
enum:  枚舉匹配(只適用於字符串)

【required】

  將age設置為必填字段,如果沒有age字段,文檔將不被保存,且出現錯誤提示

var schema = new mongoose.Schema({ age:{type:Number,required:true}, name: String,x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({name:"abc"}).save(function(err,doc){
    //Path `age` is required.
    console.log(err.errors['age'].message);
}); 

【default】

  設置age字段的默認值為18,如果不設置age字段,則會取默認值

var schema = new mongoose.Schema({ age:{type:Number,default:18}, name:String,x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({name:'a'}).save(function(err,doc){
    //{ __v: 0, name: 'a', _id: 59730d2e7a751d81582210c1, age: 18 }
    console.log(doc);
}); 

【min | max】

  將age的取值范圍設置為[0,10]。如果age取值為20,文檔將不被保存,且出現錯誤提示

var schema = new mongoose.Schema({ age:{type:Number,min:0,max:10}, name: String,x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({age:20}).save(function(err,doc){
    //Path `age` (20) is more than maximum allowed value (10).
    console.log(err.errors['age'].message);
}); 

【match】

  將name的match設置為必須存在'a'字符。如果name不存在'a',文檔將不被保存,且出現錯誤提示

var schema = new mongoose.Schema({ age:Number, name:{type:String,match:/a/},x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({name:'bbb'}).save(function(err,doc){
    //Path `name` is invalid (bbb).
    console.log(err.errors['name'].message);
}); 

【enum】

  將name的枚舉取值設置為['a','b','c'],如果name不在枚舉范圍內取值,文檔將不被保存,且出現錯誤提示

var schema = new mongoose.Schema({ age:Number, name:{type:String,enum:['a','b','c']},x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({name:'bbb'}).save(function(err,doc){
    //`bbb` is not a valid enum value for path `name`.
    console.log(err.errors['name'].message);

}); 

【validate】

  validate實際上是一個函數,函數的參數代表當前字段,返回true表示通過驗證,返回false表示未通過驗證。利用validate可以自定義任何條件。比如,定義名字name的長度必須在4個字符以上

var validateLength = function(arg){
    if(arg.length > 4){
        return true;
    }
    return false;
};
var schema = new mongoose.Schema({ name:{type:String,validate:validateLength}, age:Number,x:Number,y:Number});  
var temp = mongoose.model('temp', schema);
new temp({name:'abc'}).save(function(err,doc){
    //Validator failed for path `name` with value `abc`
    console.log(err.errors['name'].message);
}); 

 

聯表操作

  下面以一個實例的形式來介紹下mongoose中的聯表操作population

  以類別category和文章post之間的關聯為例

  其中,category的model如下所示

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const CategorySchema = new Schema(
  {
    number: { type: Number, required: true, index: true, unique: true, min:[1000000000, '位數不足'], max: [9999999999, '位數過長'] },
    name: { type: String, required: true, validate: { validator: (v) => v.trim().length, message: '名稱不能為空'} },
    description: { type: String },
    posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }],
    recommend: { type: Boolean },
    index: { type: Number }
  },
  { timestamps: true }
)

module.exports = mongoose.model('Category', CategorySchema)

  post的model如下所示

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const PostSchema = new Schema(
  {
    title: { type: String, required: true, unique: true },
    description: { type: String },
    content: { type: String },
    category: { type: Schema.Types.ObjectId, ref: 'Category', index: true },
    comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }],
    likes: [{ type: Schema.Types.ObjectId, ref: 'Like' }],
    imgUrl: { type: String },
    recommend: { type: Boolean },
    index: { type: Number }
  },
  {
    timestamps: true
  }
)

module.exports = mongoose.model('Post', PostSchema)

  在對類別的操作中, 都需要使用populate操作符顯示出所包括的posts中的title

  /* 加載所有類別 */
  app.get('/categories', (req, res) => {
    Category.find().populate('posts','title').select("number name description recommend index").exec((err, docs) => {
      if (err) return res.status(500).json({code: 0, message: err.message, err})
      return res.status(200).json({code: 1, message: '獲取類別成功', result: {docs}})
    })
  })

  /* 新增一個類別 */
  app.post('/categories', adminAuth, (req, res) => {
    new Category(req.body).save((err, doc) => {
      if (err) return res.status(500).json({code: 0, message: err.message, err})
      doc.populate({path:'posts',select:'title'}, (err, doc) => {
        if (err) return res.status(500).json({code:0, message: err.message, err})
        return res.status(200).json({code: 1, message: '新增成功', result: {doc}})
      })      
    })
  })
...

  在對文章的操作中,則需要顯示出類別category的number屬性

  /* 按照id加載一篇文章 */
  app.get('/posts/:id', (req, res) => {
    Post.findById(req.params.id).populate('category','number').exec((err, doc) => {
      if (err) return res.status(500).json({code:0, message:err.message, err})
      if (doc === null) return res.status(404).json({code:0, message:'文章不存在'})
      return res.status(200).json({code:1, message:'獲取文章成功', result:{doc}})
    })
  })

  /* 加載所有文章 */
  app.get('/posts', (req, res) => {
    Post.find().select("title likes comments recommend imgUrl index").populate('category','number').sort("-createdAt").exec((err, docs) => {
      if (err) return res.status(500).json({code: 0, message: err.message, err})
      return res.status(200).json({code: 1, message: '獲取文章成功', result: {docs}})
    })

  在新增、更新和刪除文章的操作中,都需要重建與category的關聯

// 關聯category的posts數組
fnRelatedCategory = _id => {
  Category.findById(_id).exec((err, categoryDoc) => {
    if (err) return res.status(500).json({ code: 0, message: err.message, err })
    if (categoryDoc === null) return res.status(404).json({code:0, message:'該類別不存在,請刷新后再試'})
    Post.find({ category: _id }).exec((err, postsDocs) => {
      if (err) return res.status(500).json({ code: 0, message: err.message, err })
      categoryDoc.posts = postsDocs.map(t => t._id)
      categoryDoc.save(err => {
        if (err) return res.status(500).json({ code: 0, message: err.message, err })
      })
    })
  })
}

  /* 按照id更新一篇文章 */
  app.put('/posts/:id', adminAuth, (req, res) => {
    Post.findById(req.params.id).exec((err, doc) => {
      if (err) return res.status(500).json({code: 0, message: err.message, err})
      if (doc === null) return res.status(404).json({code: 0, message: '文章不存在,請刷新后再試'})
      for (prop in req.body) {
        doc[prop] = req.body[prop]
      }
      doc.save((err) => {
        if (err) return res.status(500).json({code: 0, message: err.message, err})
        doc.populate({path:'category',select:'number'}, (err, doc) => {
          if (err) return res.status(500).json({code:0, message: err.message, err})
          fnRelatedCategory(doc.category._id)        
          return res.status(200).json({code: 1, message: '更新成功', result: {doc}})
        })
      })
    })
  })
...

 

最后

  mongoose操作基礎入門大致就是以上這些。mongoose的很多操作與mongodb的操作命令非常類似,學起來並不難。但是,由於中文資源並不完善,需要對照英文文檔進行學習,可能會稍顯吃力。而且,mongoose對mongodb做了許多擴展,增加了許多方法,需要更多耐心

  歡迎交流

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM