數據模型及基礎操作模板
為了使工程結構清晰,將數據模型(Schema, Model)的建立與增刪查改的基礎操作模板寫在一起,命名為數據庫設計中的Collection(對應於關系型數據庫中的表定義)名,並存儲在models文件夾中。
Schema與Model的建立:
Schema是Mongoose里的數據模式,可以理解為表結構定義;每個Schema會映射到MongoDB中的一個Collection,不具備操作數據庫的能力。
考慮以下代碼:
//引入mongoose模塊 var mongoose = require('mongoose'); //以json對象形式定義Schema var taskSchema = new mongoose.Schema({ userId: String, invalidFlag:Number, task: [ { _id:0, type: {type:String}, details:[{ startTime : Date, frequencyTimes : Number, frequencyUnits : String, status:Number }] } ], revisionInfo:{ operationTime:Date, userId:String } }); //導出Model var taskModel = mongoose.model('task', taskSchema);
這就定義了一個Schema和Model,映射到MongoDB中的一個Collection。實際操作過程中,需要注意以下幾點:
1. 命名規范:首字母小寫,如果命名中有多個單詞,第一個單詞首字母小寫,其他單詞首字母大寫。關於這一點,是本文這一系列的默認習慣規范,不同開發者有不同習慣。
2. 定義Schema時以json對象形式定義,鍵為屬性,值為屬性說明,關於屬性說明,至少需要定義屬性的類型(即type),如果有其他需要說明的,同樣以json的形式說明,鍵為屬性,值為說明。
3. Schema.Types: 可用的Schema Types有8種,其中String, Number, Date, Buffer, Boolean直接定義即可;Mixed, ObjectId需要引入mongoose模塊后定義;array使用中括號加元素Type定義,不說明也可以。Mixed類型可以看做嵌套類型,可以不指定內部元素的鍵,若需要指定內部元素的鍵,可以直接使用大括號聲明(如上的’revisionInfo’)
//引入mongoose模塊
var mongoose = require('mongoose'); //以json對象形式定義Schema var taskSchema = new mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, //主鍵 doctor_id: {type: mongoose.Schema.Types.ObjectId, ref:’doctor’}, //外鍵鏈接到“doctor” content: mongoose.Schema.Types.Mixed //混合或嵌套類型 });
4. 在定義Schema時的其他操作:
a) 對於全部Type有效:
required: boolean或function. 如果布爾值為真則會對模型進行驗證。
default: 設置屬性的默認值,可以是value或者function。
select: boolean 查詢時默認輸出該屬性。
validate: function, 對屬性進行自定義驗證器。
get, set: function, 自定義屬性的值
//get, set使用例子
//參考: http://mongoosejs.com/docs/schematypes.html var numberSchema = new Schema({ integerOnly: { type: Number, get: v => Math.round(v), set: v => Math.round(v) } }); var Number = mongoose.model('Number', numberSchema); var doc = new Number(); doc.integerOnly = 2.001; doc.integerOnly; // 2
b) 索引Indexes
index: Boolean 屬性是否索引
unique: Boolean 是否唯一索引
sparse: Boolean 是否稀疏索引:稀疏索引,如果索引鍵中存儲值為null,就跳過這個文檔,這些文檔將不會被索引到。不過查詢時默認是不使用稀疏索引的,需要使用hint()指定使用在模型中建立的稀疏索引。
c) 對字符串String有效
lowercase: Boolean 轉成小寫,即對值調用.toLowerCase()
uppercase: Boolean 轉成大寫,即對值調用.toUpperCase()
trim: Boolean 去掉開頭和結尾的空格,即對值調用.trim()
match: 正則表達式,生成驗證器判斷值是否符合給定的正則表達式
enum: 數組,生成驗證器判斷值是否在給定的數組中
d) 對數字Number或時間Date有效
min, max: Number或Date 生成驗證器判斷是否符合給定條件
5. 注意:
聲明Mixed類型時,以下幾種方式是等價的:
//引入mongoose模塊
var mongoose = require('mongoose'); //聲明Mixed類型 var Any = new Schema({ any: {} }); var Any = new Schema({ any: Object }); var Any = new Schema({ any: mongoose.Schema.Types.Mixed});
關於數組(Array):
a) 聲明:
//引入mongoose模塊 var mongoose = require('mongoose'); //聲明類型為Mixed的空數組 var Empty1 = new Schema({ any: [] }); var Empty2 = new Schema({ any: Array }); var Empty3 = new Schema({ any: [mongoose.Schema.Types.Mixed] }); var Empty4 = new Schema({ any: [{}] });
b) 默認屬性:
數組會隱式地含有默認值(default: []),要將這個默認值去掉,需要設定默認值(default: undefined)
如果數組被標記為(required: true),存入數據時該數組必須含有一個元素,否則會報錯。
6. 自定義Schema Type:
從mongoose.SchemaType繼承而來,加入相應的屬性到mongoose.Schema.Type中,可以使用cast()函數實現,具體例子參見:
http://mongoosejs.com/docs/customschematypes.html
7. Schema Options:對Schema進行的一系列操作,因為我沒有驗證過,就不細說了。
參考 http://mongoosejs.com/docs/guide.html
=========================================================================
在這個文件中,除了導出和編譯數據模型外,另外建立了數據庫增刪查改的基礎方法,生成函數,導出模塊供其他文件調用。
仍然以上文中的../models/task.js文件作為示例:
//設置collection同名函數,並導出模塊 function Task(task) { this.task = task; } //添加基本的增刪查改操作函數模板 //... module.exports = Task;
增:
Task.prototype.save = function(callback) { var task = this.task; var newTask = new taskModel(task); newTask.save(function(err, taskItem) { if (err) { return callback(err); } callback(null, taskItem); }); }
需要注意的是,數據庫文檔存儲方法是在Task原型鏈上修改,使用save()函數實現。在進行數據存儲的操作過程中,首先從原型對象生成實例,這里原型對象就是所要存儲的文檔。完成從原型對象生成實例的操作,使用new運算符實現,然而new運算符無法共享屬性和方法,save()函數恰恰是需要共享的方法,因此使用prototype來設置一個名為save()的函數作為文檔的通用方法。
刪:
與增加方法不同,刪除、查找及修改方法直接在Task增加方法,因為這些方法是對模型進行操作,而模型的方法已在node_modules/mongoose/lib/model.js內定義。
與刪除有關的方法:
//刪除第一個匹配conditions的文檔,要刪除所有,設置'justOne' = false
remove(conditions, [callback]);
//刪除第一個匹配conditions的文檔,會忽略justOne操作符
deleteOne(conditions, [callback]);
//刪除所有匹配conditions的文檔,會忽略justOne操作符
deleteMany(conditions, [callback]);
//實現MongoDB中的findAndModify remove命令,並將找到的文檔傳入callback中
//options: 'sort', 'maxTimeMS', 'select'
findOneAndRemove(conditions, [options], [callback]);
//以主鍵作為查詢條件刪除文檔,並將找到的文檔傳入callback中
findByIdAndRemove(id, [options], [callback]);
Task.removeOne = function(query, callback, opts) {
var options = opts || {}; taskModel .findOneAndRemove(query, options, function(err, task) { if (err) { return callback(err); } callback(null, task); }); };
這個例子中,將導出的函數取名為Task.removeOne(), 在傳入參數時,將[option]放到了最后,這樣做的本意,是因為實際應用時,options往往是空的,不需要傳入,這樣做就可以在寫controller時直接省略而不用空字符串占位。但事實上,在model.js中定義時,已經做了處理:conditions必須傳入,且不能為function, 當第二個參數options是function時,將這個function認為是callback, 並將options設置為undefined
if (arguments.length === 1 && typeof conditions === 'function') {
var msg = 'Model.findOneAndRemove(): First argument must not be a function.\n\n' + ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n' + ' ' + this.modelName + '.findOneAndRemove(conditions)\n' + ' ' + this.modelName + '.findOneAndRemove()\n'; throw new TypeError(msg); } if (typeof options === 'function') { callback = options; options = undefined; }
改:
與修改有關的方法:
//更新文檔而不返回他們
//option: ‘upsert’: if true, 如果沒有匹配條件的文檔則新建
//option: ‘multi’: if true, 更新多文檔
//option: ‘runValidators’, if true, 在更新之前進行模型驗證
//option: ‘setDefaultsOnInsert’, 如果此操作符與’upsert’同時為true, 將schema中的默認值新建到新文檔中
//注意不要使用已存在的實例作為更新子句,有可能導致死循環
//注意更新子句中不要存在_id字段,因為MongoDB不允許這樣做
//使用update時,值會轉換成對應type, 但是defaults, setters, validators, middleware不會應用,如果要應用這些,應使用findOne()然后在回調函數里調用.save()函數
update(conditions, doc, [options], [callback]);
//忽略multi操作符,將所有符合conditions的文檔修改
updateMany(conditions, doc, [options], [callback]);
//忽略multi操作符,僅將第一個符合conditions的文檔修改
updateOne(conditions, doc, [options], [callback]);
//使用新文檔替換而不是修改
replaceOne(conditions, doc, [options], [callback]);
//找到匹配的文檔,並根據[update]更新文檔,將找到的文檔傳入[callback]
//option: ‘new’: if true,返回更新后的文檔
//’upsert’, ‘runValidators’, ‘setDefaultsOnInsert’, ’sort’, ‘select’等操作符也可用
findOneAndUpdate([conditions], [update], [options], [callback]);
//通過主鍵找到匹配的文檔,並根據[update]更新文檔,將找到的文檔傳入[callback]
findByIdAndUpdate(id, [update], [options], [callback]);
Task.updateOne = function(query, obj, callback, opts, populate) {
var options = opts || {}; var populate = populate || ''; taskModel .findOneAndUpdate(query, obj, options) .populate(populate) .exec(function(err, uptask) { if(err){ return callback(err); } callback(null, uptask); }); }; Task.update = function(query, obj, callback, opts, populate) { var options = opts || {}; var populate = populate || ''; taskModel .update(query, obj, options) .populate(populate) .exec(function(err, uptask) { if(err){ return callback(err); } callback(null, uptask); }); };
與刪除方法不同,callback不傳入.update()或.findOneAndUpdate()中,而在之后調用了.exec()中傳入了一個回調函數,如果err有內容則返回err, 否則返回uptask,也就是MongoDB的返回。這樣的處理,可以不需要等待MongoDB的響應。
populate是聯表查詢時使用的參數,將在之后的內容提到。
查:
與查詢有關的方法:
//conditions會在命令發送前自動被轉成對應的SchemaTypes
find(conditions, [projection], [options], [callback]);
//通過_id查詢到一條文檔
findById(id, [projection], [options], [callback]);
//查詢一條文檔,如果condition = null or undefined, 會返回任意一條文檔
findOne([conditions], [projection], [options], [callback]);
Task.getOne = function(query, callback, opts, fields, populate) {
var options = opts || {}; var fields = fields || null; var populate = populate || ''; taskModel .findOne(query, fields, opts) .populate(populate) .exec(function(err, taskInfo) { if(err){ return callback(err); } callback(null, taskInfo); }); }; Task.getSome = function(query, callback, opts, fields, populate) { var options = opts || {}; var fields = fields || null; var populate = populate || ''; taskModel .find(query, fields, options) .populate(populate) .exec(function(err, tasks) { if(err) { return callback(err); } callback(null, tasks); }); };
在構造出的.getOne()和.getSome()函數的傳入參數中,可以看到option, field, populate在callback后面,因為最基本的情況是只有query和callback傳入,而后面的較少用到。而在一些要求復雜的查詢中,這三者是必不可少的。
雖然查詢最為復雜,不過都是通過.find()與.findOne()與各種操作符組合而成。同樣因為最基本的參數是condition與callback, 因此在導出函數時將這兩個參數放在最前面。值得注意的是,當查詢不到文檔時,.findOne()返回null, .find()返回空數組,這使得在調用getOne()函數時的某些情況下需要進行必要的輸出驗證,否則會報錯引起程序崩潰。