什麼是Mongoose?
Mongoose是一款給node.js用的MongoDB ODM。Mongoose提供一個直覺的,基於模式的解決方案來對應用程序數據進行建模,我們可以使用JavaScript的思維去操作,而不用轉向數據庫語言的思維。
安裝Mongoose
npm i mongoose
連結Mongoose與MongoDB
/test的地方可以自定義,相等於MongoDB裡的db名稱。
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true, useUnifiedTopology: true});
Schema
設定Schema
Schema就像是文件資料的綱要,可以定義每個文檔中存儲的SchemaTypes。
方法1比較常用,具有擴展性,在type之後還可以加入其他property,而方法2則無法。
const movieSchema = new mongoose.Schema({
title: {
type: String //方法1
}
year: Number //方法2
})
SchemaType
- String
- Number
- Date
- Buffer
- Buffer通常用於二進制數據
- Boolean
- 默認情況下,以下皆會轉成
true
:true、'true'、1、'1'、'yes' - 默認情況下,以下皆會轉成
false
:false、'false'、0、'0'、'no' - Mixed
- 沒有定義類型的數據類型
- { type: {}} = {type: Schema.Types.Mixed}
- 用doc.markModified()來更新數據
- ObjectId
- MongoDB與Mongoose的id皆為物件
- ex:
const testSchema = new mongoose.Schema({ testId: mongoose.ObjectId })
- Array
- 用[]括起來
- ex:
const schema = new mongoose.Schema({ genre:['action', 'comedy']})
const Empty2 = new Schema({ any: Array });
= Mixed類型
- Decimal128
- 用於128-bit decimal floating points中,不要實例化,應該用
mongoose.Types.Decimal128
- 用於128-bit decimal floating points中,不要實例化,應該用
- Map
- MongooseMap是JS Map class的subclass,必須用String
添加property到SchemaType
除了type以外,還可以加入property:
const movieSchema = new mongoose.Schema({
title: {
type: String,
require: true
},
year: {
type: Number
},
score: {
type: Number,
min: 0,
max: 10
}
})
- required:
- boolean或是function,true的話會將property設置為required validator。就是validation。
- default:
- Any或是function。預設值
- select:
- boolean或是specifies default projections for queries
- validate:
- function。就是validation。
- get:
- function,用Object.defineProperty()定義custom getter
- set:
- function,用Object.defineProperty()定義custom setter
- alias:
- string, mongoose >= 4.10.0 only. Defines a virtual with the given name that gets/sets this path.
- immutable:
- boolean,定義path為immutable。
- transform:
- function, Mongoose calls this function when you call Document#toJSON() function, including when you JSON.stringify() a document.
- Indexes
- index: boolean
- unique: boolean
- sparse: boolean
- String
- lowercase: boolean
- uppercase: boolean
- trim: boolean
- match: RegExp
- enum: Array, creates a validator that checks if the value is in the given array.
- minLength: Number,檢查長度
- maxLength: Number,檢查長度
- populate: Object, sets default populate options
- Number
- min: Number,檢查大小
- max: Number,檢查大小
- enum: Array, creates a validator that checks if the value is strictly equal to one of the values in the given array.
- populate: Object, sets default populate options
- Date
- min: Date
- max: Date
- ObjectId
- populate: Object, sets default populate options
Update時需要注意的點
使用update()、updateOne()、updateMany()、findOneAndUpdate()時需注意設定{runValidators: true}
,否則原本設定的validators不會運作。
Movie.update({ title: 'joker' }, { score: -9 }, { new: true, runValidators: true })
//Error: Validation failed: score: Path `score` (-9) is less than minimum allowed value (0).
使用findOneAndUpdate()時會出現deprecation warnings,記得在connect中加入mongoose.connect(uri, { useFindAndModify: false })
Instance methods(實例方法)
在Schema中建立實例方法(Instance methods),可以讓我們添加自己想到的function。
範例一:切換onSale
//setting schema
const productSchema = new mongoose.Schema({
name: {
type: String
},
onSale: {
type: Boolean,
default: false
}
})
//instance methods
productSchema.methods.toggleFuc = function() {
this.onSale = !this.onSale;
return this.save()
}
//model
const Product = mongoose.model('Product', productSchema)
const pen = new Product({
name: 'pen',
onSale: false
})
pen.save();
//use function
const findProduct = async() => {
const foundProduct = await Product.findOne({ name: 'pen' })
console.log(foundProduct)
await foundProduct.toggleFuc();
console.log(foundProduct)
}
findProduct()
//在node運行
{ onSale: false, _id: 600c16aa2f5c42010d1aedbe, name: 'pen', __v: 0 }
{ onSale: true, _id: 600c16aa2f5c42010d1aedbe, name: 'pen', __v: 0 }
範例二:添加category方法
//setting schema
const productSchema = new mongoose.Schema({
name: {
type: String
},
category: {
type: []
}
})
//instance methods
productSchema.methods.addCategory = function(addProductCategory) {
this.category.push(addProductCategory)
return this.save()
}
//model
const Product = mongoose.model('Product', productSchema)
const pen = new Product({
name: 'pen'
})
pen.save();
//use function
const findProduct = async() => {
const foundProduct = await Product.findOne({ name: 'pen' })
console.log(foundProduct)
await foundProduct.addCategory('stationery');
console.log(foundProduct)
}
findProduct()
//在node運行
{
category: [],
_id: 600c16aa2f5c42010d1aedbe,
onSale: true,
name: 'pen',
__v: 0
}
{
category: [ 'stationery' ],
_id: 600c16aa2f5c42010d1aedbe,
onSale: true,
name: 'pen',
__v: 1
}
Statics(靜態方法)
靜態方法(Statics)是從整個模型(model)的上下文(context)運行的方法,而不是像實例方法(instance methods),實例方法用於對特定實例進行操作。
那麼什麼時候用靜態方法還是實例方法呢?舉例來說,如果想獲取一個用戶的全名,則它只涉及到一個用戶,所以應該用實例方法,另一方面,像是要查詢名字皆為A開頭的用戶,這類涉及到整體的查詢,則應該使用靜態方法。簡單來說,通常靜態方法會涉及到查詢、新增、修改、刪除。
靜態方法的兩種寫法:
//1. Add a function property to schema.statics
animalSchema.statics.updateType = function() {
return Animal.updateMany({}, { type: 'dog' })
}
//2. Call the Schema#static() function
animalSchema.static('updateType', function() {
return Animal.updateMany({}, { type: 'dog' })
})
範例一:創建一個Statics查詢type
const animalSchema = new mongoose.Schema({
name: {
type: String
},
type: {
type: String
}
})
//Statics
animalSchema.statics.findByType = function(typeName) {
return this.find({ type: new RegExp(typeName, 'i') })
}
const Animal = mongoose.model('Animal', animalSchema)
const dog = new Animal({
name: 'Ruby',
type: 'dog'
})
dog.save()
const cat = new Animal({
name: 'Soe',
type: 'cat'
})
cat.save()
//運行 node index.js
Animal.findByType('cat', function(err, animals) {
console.log(animals)
}).then(data => console.log(data))
//{ _id: 600d5fbae917501c3aacf28e, name: 'Soe', type: 'cat', __v: 0 }
Mongoose Virtuals
Virtual可以幫助我們在Schema新增property,而這些property並不存在在database中,但我們可以使用和讀取。
範例一:創建一個virtual,可以將mail中的domain獨立出來
const virtualSchema = new mongoose.Schema({
mail: {
type: String
}
})
virtualSchema.virtual('domain').get(function() {
return this.mail.slice(this.mail.indexOf('@') + 1)
})
const User = mongoose.model('User', virtualSchema)
const userMail = new User({
mail: 'test_virtual@gmail.com'
})
userMail.save()
console.log(userMail.domain)
//運行node index.js
//gmail.com
範例二:創建一個virtual,可以將lastName和firstName結合成fullName
userNameSchema.virtual('fullName').get(function() {
return `${this.lastName} ${this.firstName}`
})
const User_name = mongoose.model('User_name', userNameSchema)
const userName = new User_name({
lastName: 'Teagan',
firstName: 'Hsu'
})
userName.save()
const fuc = async() => {
let doc = await userName;
console.log(doc.fullName)
}
fuc()
//運行node index.js
//Teagan Hsu
Virtual Setters
使用Setter可以一次設置多個property。舉例來說,我們想同時設置firstName和lastName這兩個屬性,就可以使用Virtual Setters。
範例一:創建一個virtual Setter,同時設置lastName和firstName兩個屬性
const userNameSchema = new mongoose.Schema({
lastName: {
type: String
},
firstName: {
type: String
}
})
userNameSchema.virtual('fullName').get(function() {
return `${this.lastName} ${this.firstName}`
}).set(function(v) {
const firstName = v.slice(0, v.indexOf(' '))
const lastName = v.slice(v.indexOf(' ') + 1)
this.set({ firstName, lastName })
})
const User_name = mongoose.model('User_name', userNameSchema)
const userName = new User_name()
userName.fullName = 'Teagan Hsu'
userName.save()
console.log(userName)
//在node運行index.js(lastName和firstName一起被設置好了)
//{ _id: 600d7e1e3fa5a029a2a53422, firstName: 'Teagan', lastName: 'Hsu' }
Virtuals in JSON
預設情況下,Mongoose在將文檔轉換為JSON時不會包含virtuals,所以需要將toJSON schema設置為{ virtuals: true }
。
範例一:轉換成JSON檔
const virtualSchema = new mongoose.Schema({
mail: {
type: String
}
}, {
toJSON: {
virtuals: true
}
})
virtualSchema.virtual('domain').get(function() {
return this.mail.slice(this.mail.indexOf('@') + 1)
})
const User = mongoose.model('User', virtualSchema)
const userMail = new User({
mail: 'test_virtual@gmail.com'
})
userMail.save()
console.log(userMail.toJSON().domain) //gmail.com
console.log(JSON.stringify(userMail))
//{"_id":"600d8485beb5612cbab6c15c","mail":"test_virtual@gmail.com","domain":"gmail.com","id":"600d8485beb5612cbab6c15c"}
virtual的限制(Limitations)
因為virtual沒有儲存在database中所以無法查詢。
const doc = async() => {
const userMail = await User.findOne({ domain: 'gmail.com' })
console.log(userMail)
}
doc()
//null
Middleware
Middleware (也稱作pre and post hooks)是在非同步函數執行期間傳遞控制的函數。
Types of Middleware
- validate
- save
- remove
- updateOne
- deleteOne
- init (note: init hooks are synchronous
Pre
當每個middleware調用下一個middleware時,Pre middleware function會先執行。舉例來說,我的用戶想要刪除User帳號,那我同時也想刪掉用戶所有的posts和comments,這時可以使用:
//在save保存之前,先執行刪除posts和comments的行為...
const schema = new Schema(..);
schema.pre('save', function(next) {
// do something
next();
});
Post
post middleware會執行在所有hooked method與pre middleware之後。
範例一:小測驗,最後輸出的名字是?
const schema = new mongoose.Schema({
name: String
})
schema.pre('save', async function() {
this.name = 'Zoe'
})
schema.post('save', async function() {
this.name = 'Ginny'
})
const User = mongoose.model('User', schema)
const user1 = new User({
name: 'Teagan'
})
user1.save().then(res => console.log(res))
解答:
一開始為name: 'Teagan'
,但在save前會因pre middleware而變成name = 'Zoe'
,而在pre middleware之後執行的是post middleware,所以答案是name = 'Ginny'
參考資料:
- Mongoose v5.11.13: Schemas
- Mongoose v5.11.13: Deprecation Warnings
- mongoose修改Mixed(混合)类型
- What is the use of mongoose methods and statics?
- can any one explain meaning of mixed and buffer data type in mongoose?
- Mongoose findOneAndUpdate and runValidators not working
- How to Add Instance Methods with Mongoose
- How to Add Static Methods with Mongoose