前言:本學習參考文檔僅供參考,如有問題,師請雅正
一、快速通道
1.1 名詞解釋
-
Schema
: 一種以文件形式存儲的數據庫模型骨架,不具備數據庫的操作能力 -
Model
: 由Schema
發佈生成的模型,具有抽象屬性和行爲的數據庫操作對 -
Entity
: 由Model
創建的實體,他的操作也會影響數據庫
注意:
1.本學習文檔採用嚴格命名方式來區別不同對象,例如:
var PersonSchema; //Person的文本屬性
var PersonModel; //Person的數據庫模型
var PersonEntity; //Person實體
2.Schema
、Model
、Entity
的關係請牢記,Schema
生成Model
,Model
創造Entity
,Model
和Entity
都可對數據庫操作造成影響,但Model
比Entity
更具操作性。
1.2 準備工作
2.在項目只能夠創建一個數據庫連接,如下:
var mongoose = require('mongoose'); //引用mongoose模塊
var db = mongoose.createConnection('localhost','test'); //創建一個數據庫連接
3.打開本機localhost
的test
數據庫時,我們可以監測是否有異常
db.on('error',console.error.bind(console,'連接錯誤:'));
db.once('open',function(){
//一次打開記錄
});
注意:
成功開啓數據庫後,就可以執行數據庫相應操作,假設以下代碼都在回調中處理
4.定義一個Schema
var PersonSchema = new mongoose.Schema({
name:String //定義一個屬性name,類型爲String
});
5.將該Schema
發佈爲Model
var PersonModel = db.model('Person',PersonSchema);
//如果該Model已經發布,則可以直接通過名字索引到,如下:
//var PersonModel = db.model('Person');
//如果沒有發佈,上一段代碼將會異常
6.用Model
創建Entity
var personEntity = new PersonModel({name:'Krouky'});
//打印這個實體的名字看看
console.log(personEntity.name); //Krouky
7.我們甚至可以爲此Schema
創建方法
//爲Schema模型追加speak方法
PersonSchema.methos.speak = function(){
console.log('我的名字叫'+this.name);
}
var PersonModel = db.model('Person',PersonSchema);
var personEntity = new PersonModel({name:'Krouky'});
personEntity.speak();//我的名字叫Krouky
8.Entity
是具有具體的數據庫操作CRUD
的
personEntity.save(); //執行完成後,數據庫就有該數據了
9.如果要執行查詢,需要依賴Model
,當然Entity
也是可以做到的
PersonModel.find(function(err,persons){
//查詢到的所有person
});
注意:
1. 具體的如何配置Schema
、Model
以及Model
和Entity
的相關操作,我們會在後面進行
2. Model
和Entity
都有能影響數據庫的操作,但仍有區別,後面我們也會做解釋
二、新手指引
如果您還不清楚Mongoose
是如何工作的,請參看第一章快速通道快速瀏覽他的用法吧
1. Schema——純潔的數據庫原型
1.1 什麼是Schema
- 我理解
Schema
僅僅只是一斷代碼,他書寫完成後程序依然無法使用,更無法通往數據庫端 - 他僅僅只是數據庫模型在程序片段中的一種表現,或者是數據屬性模型
1.2 如何定義Schema
var BlogSchema = new Schema({
title:String,
author:String
//new Schema()中傳入一個JSON對象,該對象形如 xxx:yyyy ,
/xxx是一個字符串,定義了屬性,yyy是一個Schema.Type,定義了屬性類型
});
1.3 什麼是Schema.Type
Schema.Type
是由Mongoose
內定的一些數據類型,基本數據類型都在其中,他也內置了一些Mongoose
特有的Schema.Type
。當然,你也可以自定義Schema.Type
,只有滿足Schema.Type
的類型才能定義在Schema
內。
1.4 Schema.Types
NodeJS
中的基本數據類型都屬於Schema.Type
,另外Mongoose
還定義了自己的類型
//舉例:
var ExampleSchema = new Schema({
name:String,
binary:Buffer,
living:Boolean,
updated:Date,
age:Number,
mixed:Schema.Types.Mixed, //該混合類型等同於nested
_id:Schema.Types.ObjectId, //主鍵
_fk:Schema.Types.ObjectId, //外鍵
array:[],
arrOfString:[String],
arrOfNumber:[Number],
arrOfDate:[Date],
arrOfBuffer:[Buffer],
arrOfBoolean:[Boolean],
arrOfMixed:[Schema.Types.Mixed],
arrOfObjectId:[Schema.Types.ObjectId]
nested:{
stuff:String,
}
});
1.5 關於Buffer
Buffer
和ArrayBuffer
是Nodejs
兩種隱藏的對象,相關內容請查看NodeJS-API
1.6 關於Mixed
Schema.Types.Mixed
是Mongoose
定義個混合類型,該混合類型如果未定義具體形式。因此,如果定義具體內容,就直接使用{}
來定義,以下兩句等價
var AnySchema = new Schema({any:{}});
var AnySchema = new Schema({any:Schema.Types.Mixed});
混合類型因爲沒有特定約束,因此可以任意修改,一旦修改了原型,則必須調用markModified()
person.anything = {x:[3,4,{y:'change'}]}
person.markModified('anything');//傳入anything,表示該屬性類型發生變化
person.save();
1.7 關於ObjectId
主鍵,一種特殊而且非常重要的類型,每個Schema
都會默認配置這個屬性,屬性名爲_id
,除非自己定義,方可覆蓋
var mongoose = require('mongoose');
var ObjectId = mongoose.Schema.Types.ObjectId;
var StudentSchema = new Schema({}); //默認會有_id:ObjectId
var TeacherSchema = new Schema({id:ObjectId});//只有id:ObjectId
該類型的值由系統自己生成,從某種意義上幾乎不會重複,生成過程比較複雜,有興趣的朋友可以查看源碼。
1.8 關於Array
Array
在JavaScript
編程語言中並不是數組,而是集合,因此裏面可以存入不同的值,以下代碼等價:
var ExampleSchema1 = new Schema({array:[]});
var ExampleSchema2 = new Schema({array:Array});
var ExampleSchema3 = new Schema({array:[Schema.Types.Mixed]});
var ExampleSchema4 = new Schema({array:[{}]});
1.9 附言
Schema
不僅定義了文檔結構
和使用性能
,還可以有擴展插件
、實例方法
、靜態方法
、複合索引
、文檔生命週期鉤子
Schema
可以定義插件,並且插件具有良好的可拔插性,請有興趣的讀者繼續往後閱讀或者查閱官方資料。
2. Schema的擴展
2.1 實例方法
有的時候,我們創造的Schema
不僅要爲後面的Model
和Entity
提供公共的屬性,還要提供公共的方法。
下面例子比快速通道的例子更加高級,可以進行高級擴展:
var PersonSchema = new Schema({name:String,type:String});
//查詢類似數據
PersonSchema.methods.findSimilarTypes = function(cb){
return this.model('Person').find({type:this.type},cb);
}
使用如下:
var PersonModel = mongoose.model('Person',PersonSchema);
var krouky = new PersonSchema({name:'krouky',type:'前端工程師'});
krouky.findSimilarTypes(function(err,persons){
//persons中就能查詢到其他前端工程師
});
2.2 靜態方法
靜態方法在Model
層就能使用,如下:
PersonSchema.statics.findByName = function(name,cb){
this.find({name:new RegExp(name,'i'),cb});
}
var PersonModel = mongoose.model('Person',PersonSchema);
PersonModel.findByName('krouky',function(err,persons){
//找到所有名字叫krouky的人
});
2.3 索引
索引或者複合索引能讓搜索更加高效,默認索引就是主鍵索引ObjectId
,屬性名爲_id
, 索引會作爲一個專題來講解
2.4 虛擬屬性
Schema
中如果定義了虛擬屬性,那麼該屬性將不寫入數據庫,例如:
var PersonSchema = new Schema({
name:{
first:String,
last:String
}
});
var PersonModel = mongoose.model('Person',PersonSchema);
var krouky = new PersonModel({
name:{first:'krouky',last:'han'}
});
如果每次想使用全名就得這樣
console.log(krouky.name.first + ' ' + krouky.name.last);
顯然這是很麻煩的,我們可以定義虛擬屬性
:
PersonSchema.virtual('name.full').get(function(){
return this.name.first + ' ' + this.name.last;
});
那麼就能用krouky.name.full
來調用全名了,反之如果知道full,也可以反解first
和last
屬性
PersonSchema.virtual('name.full').set(function(name){
var split = name.split(' ');
this.name.first = split[0];
this.name.last = split[1];
});
var PersonModel = mongoose.model('Person',PersonSchema);
var krouky = new PersonModel({});
krouky.name.full = 'krouky han';//會被自動分解
console.log(krouky.name.first);//krouky
2.5 配置項
在使用new Schema(config)
時,我們可以追加一個參數options
來配置Schema
的配置,形如:
var ExampleSchema = new Schema(config,options);
或者使用
var ExampleSchema = new Schema(config);
ExampleSchema.set(option,value);
可供配置項有:safe
、strict
、capped
、versionKey
、autoIndex
2.5.1 safe——安全屬性(默認安全)
一般可做如下配置:
new Schema({...},{safe:true});
當然我們也可以這樣
new Schema({...},{safe:{j:1,w:2,wtimeout:10000}});
j
表示做1份日誌,w
表示做2個副本(尚不明確),超時時間10秒
2.5.2 strict——嚴格配置(默認啓用)
確保Entity
的值存入數據庫前會被自動驗證,如果你沒有充足的理由,請不要停用,例子:
var ThingSchema = new Schema({a:String});
var ThingModel = db.model('Thing',SchemaSchema);
var thing = new Thing({iAmNotInTheThingSchema:true});
thing.save();//iAmNotInTheThingSchema這個屬性將無法被存儲
如果取消嚴格選項,iAmNotInTheThingSchema
將會被存入數據庫
該選項也可以在構造實例時使用,例如:
var ThingModel = db.model('Thing');
var thing1 = new ThingModel(doc,true); //啓用嚴格
var thing2 = new ThingModel(doc,false); //禁用嚴格
注意:
strict
也可以設置爲throw
,表示出現問題將會拋出錯誤
2.5.3 shardKey
需要mongodb
做分佈式,纔會使用該屬性
2.5.4 capped——上限設置
如果有數據庫的批量操作,該屬性能限制一次操作的量,例如:
new Schema({...},{capped:1024}); //一次操作上線1024條數據
當然該參數也可是JSON對象,包含size、max、autiIndexId屬性
new Schema({...},{capped:{size:1024,max:100,autoIndexId:true}});
2.5.5 versionKey——版本鎖
版本鎖是Mongoose
默認配置(__v屬性)的,如果你想自己定製,如下:
new Schema({...},{versionKey:'__someElse'});
此時存入數據庫的版本鎖就不是__v
屬性,而是__someElse
,相當於是給版本鎖取名字。
具體怎麼存入都是由Mongoose
和MongoDB
自己決定,當然,這個屬性你也可以去除
new Schema({...},{versionKey:false});
除非你知道你在做什麼,並且你知道這樣做的後果
2.5.6 autoIndex——自動索引
該內容將在索引章節單獨講解
3. Documents
Document
是與MongoDB
文檔一一對應的模型,Document
可等同於Entity
,具有屬性和操作性
注意:
Document
的`CRUD都必須經過嚴格驗證的,參看2.5.2 Schema的strict嚴格配置
3.1 查詢
查詢內容過多,專題講解
3.2 更新
有許多方式來更新文件,以下是常用的傳統方式:
PersonModel.findById(id,function(err,person){
person.name = 'MDragon';
person.save(function(err){});
});
這裏,利用Model
模型查詢到了person
對象,該對象屬於Entity
,可以有save操作,如果使用
Model`操作,需注意:
PersonModel.findById(id,function(err,person){
person.name = 'MDragon';
var _id = person._id; //需要取出主鍵_id
delete person._id; //再將其刪除
PersonModel.update({_id:_id},person,function(err){});
//此時才能用Model操作,否則報錯
});
update
第一個參數是查詢條件,第二個參數是更新的對象,但不能更新主鍵,這就是爲什麼要刪除主鍵的原因。
當然這樣的更新很麻煩,可以使用$set
屬性來配置,這樣也不用先查詢,如果更新的數據比較少,可用性還是很好的:
PersonModel.update({_id:_id},{$set:{name:'MDragon'}},function(err){});
需要注意,Document
的CRUD
操作都是異步執行,callback
第一個參數必須是err
,而第二個參數各個方法不一樣,update
的callback
第二個參數是更新的數量,如果要返回更新後的對象,則要使用如下方法
Person.findByIdAndUpdate(_id,{$set:{name:'MDragon'}},function(err,person){
console.log(person.name); //MDragon
});
類似的方法還有findByIdAndRemove
,如同名字,只能根據id查詢並作update
/remove
操作,操作的數據僅一條
3.3 新增
如果是Entity
,使用save
方法,如果是Model
,使用create
方法
//使用Entity來增加一條數據
var krouky = new PersonModel({name:'krouky'});
krouky.save(callback);
//使用Model來增加一條數據
var MDragon = {name:'MDragon'};
PersonModel.create(MDragon,callback);
兩種新增方法區別在於,如果使用Model
新增時,傳入的對象只能是純淨的JSON
對象,不能是由Model
創建的實體,原因是:由Model
創建的實體krouky
雖然打印是隻有{name:'krouky'}
,但是krouky
屬於Entity
,包含有Schema
屬性和Model
數據庫行爲模型。如果是使用Model
創建的對象,傳入時一定會將隱藏屬性也存入數據庫,雖然3.x
追加了默認嚴格屬性,但也不必要增加操作的報錯
3.4 刪除
和新增一樣,刪除也有2種方式,但Entity
和Model
都使用remove
方法
4.Sub Docs
如同SQL
數據庫中2張表有主外關係,Mongoose
將2個Document
的嵌套叫做Sub-Docs
(子文檔)
簡單的說就是一個Document
嵌套另外一個Document
或者Documents
:
var ChildSchema1 = new Schema({name:String});
var ChildSchema2 = new Schema({name:String});
var ParentSchema = new Schema({
children1:ChildSchema1, //嵌套Document
children2:[ChildSchema2] //嵌套Documents
});
Sub-Docs
享受和Documents
一樣的操作,但是Sub-Docs
的操作都由父類去執行
var ParentModel = db.model('Parent',parentSchema);
var parent = new ParentModel({
children2:[{name:'c1'},{name:'c2'}]
});
parent.children2[0].name = 'd';
parent.save(callback);
parent
在執行保存時,由於包含children2
,他是一個數據庫模型對象,因此會先保存chilren2[0]
和chilren2[1]
。
如果子文檔在更新時出現錯誤,將直接報在父類文檔中,可以這樣處理:
ChildrenSchema.pre('save',function(next){
if('x' === this.name) return next(new Error('#err:not-x'));
next();
});
var parent = new ParentModel({children1:{name:'not-x'}});
parent.save(function(err){
console.log(err.message); //#err:not-x
});
4.1 查詢子文檔
如果children
是parent
的子文檔,可以通過如下方法查詢到children
var child = parent.children.id(id);
4.2 新增、刪除、更新
子文檔是父文檔的一個屬性,因此按照屬性的操作即可,不同的是在新增父類的時候,子文檔是會被先加入進去的。
如果ChildrenSchema
是臨時的一個子文檔,不作爲數據庫映射集合,可以這樣:
var ParentSchema = new Schema({
children:{
name:String
}
});
//其實就是匿名混合模式
5.Model
5.1 什麼是Model
Model
模型,是經過Schema
構造來的,除了Schema
定義的數據庫骨架以外,還具有數據庫行爲模型,他相當於管理數據庫屬性、行爲的類
5.2 如何創建Model
你必須通過Schema
來創建,如下:
//先創建Schema
var TankSchema = new Schema({
name:'String',
size:'String'
});
//通過Schema創建Model
var TankModel = mongoose.model('Tank',TankSchema);
5.2 操作Model
該模型就能直接拿來操作,具體查看API
,例如:
var tank = {'something',size:'small'};
TankModel.create(tank);
注意:
你可以使用Model
來創建Entity
,Entity
實體是一個特有Model
具體對象,但是他並不具備Model
的方法,只能用自己的方法。
//通過Model創建Entity
var tankEntity = new TankModel('someother','size:big');
tankEntity.save();
6.Query
查詢是數據庫中運用最多也是最麻煩的地方,這裏對Query
解讀的並不完善,僅僅是自己的一點領悟而已。
6.1 查詢的方式
通常有2種查詢方式,一種是直接查詢
,一種是鏈式查詢
(2種查詢都是自己命名的)
6.1.1 直接查詢
在查詢時帶有回調函數的,稱之爲直接查詢,查詢的條件往往通過API
來設定,例如:
PersonModel.findOne({'name.last':'dragon'},'some select',function(err,person){
//如果err==null,則person就能取到數據
});
具體的查詢參數,請查詢API
6.1.2 鏈式查詢
在查詢時候,不帶回調,而查詢條件通過API
函數來制定,例如:
var query = PersonModel.findOne({'name.last':'dragon'});
query.select('some select');
query.exec(function(err,pserson){
//如果err==null,則person就能取到數據
});
這種方式相對直接查詢,分的比較明細,如果不帶callback
,則返回query
,query
沒有執行的預編譯查詢語句,該query
對象執行的方法都將返回自己,只有在執行exec
方法時才執行查詢,而且必須有回調。
因爲query
的操作始終返回自身,我們可以採用更形象的鏈式寫法
Person
.find({ occupation: /host/ })
.where('name.last').equals('Ghost')
.where('age').gt(17).lt(66)
.where('likes').in(['vaporizing', 'talking'])
.limit(10)
.sort('-occupation')
.select('name occupation')
.exec(callback);
7.Validation
數據的存儲是需要驗證的,不是什麼數據都能往數據庫裏丟或者顯示到客戶端的,數據的驗證需要記住以下規則:
- 驗證始終定義在
SchemaType
中 - 驗證是一個內部中間件
- 驗證是在一個
Document
被保存時默認啓用的,除非你關閉驗證 - 驗證是異步遞歸的,如果你的
SubDoc
驗證失敗,Document
也將無法保存 - 驗證並不關心錯誤類型,而通過
ValidationError
這個對象可以訪問
7.1 驗證器
required
非空驗證min
/max
範圍驗證(邊值驗證)enum
/match
枚舉驗證/匹配驗證validate
自定義驗證規則
以下是綜合案例:
var PersonSchema = new Schema({
name:{
type:'String',
required:true //姓名非空
},
age:{
type:'Nunmer',
min:18, //年齡最小18
max:120 //年齡最大120
},
city:{
type:'String',
enum:['北京','上海'] //只能是北京、上海人
},
other:{
type:'String',
validate:[validator,err] //validator是一個驗證函數,err是驗證失敗的錯誤信息
}
});
7.2 驗證失敗
如果驗證失敗,則會返回err
信息,err
是一個對象該對象屬性如下
err.errors //錯誤集合(對象)
err.errors.color //錯誤屬性(Schema的color屬性)
err.errors.color.message //錯誤屬性信息
err.errors.path //錯誤屬性路徑
err.errors.type //錯誤類型
err.name //錯誤名稱
err.message //錯誤消息
一旦驗證失敗,Model
和Entity
都將具有和err
一樣的errors
屬性
8.Middleware中間件
8.1 什麼是中間件
中間件是一種控制函數,類似插件,能控制流程中的init
、validate、
save、
remove`方法
8.2 中間件的分類
中間件分爲兩類
8.2.1 Serial串行
串行使用pre
方法,執行下一個方法使用next
調用
var schema = new Schema(...);
schema.pre('save',function(next){
//做點什麼
next();
});
8.2.2 Parallel並行
並行提供更細粒度的操作
var schema = new Schema(...);
schema.pre('save',function(next,done){
//下一個要執行的中間件並行執行
next();
doAsync(done);
});
8.3 中間件特點
一旦定義了中間件,就會在全部中間件執行完後執行其他操作,使用中間件可以霧化模型,避免異步操作的層層迭代嵌套
8.4 使用範疇
- 複雜的驗證
- 刪除有主外關聯的
doc
- 異步默認
- 某個特定動作觸發異步任務,例如觸發自定義事件和通知
例如,可以用來做自定義錯誤處理
schema.pre('save',function(next){
var err = new Eerror('some err');
next(err);
});
entity.save(function(err){
console.log(err.message); //some err
});