Web開發學習筆記23 — 開始使用數據庫(Mongoose)


Posted by Teagan Hsu on 2021-01-25

什麼是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
  • 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'


參考資料:

  1. Mongoose v5.11.13: Schemas
  2. Mongoose v5.11.13: Deprecation Warnings
  3. mongoose修改Mixed(混合)类型
  4. What is the use of mongoose methods and statics?
  5. can any one explain meaning of mixed and buffer data type in mongoose?
  6. Mongoose findOneAndUpdate and runValidators not working
  7. How to Add Instance Methods with Mongoose
  8. How to Add Static Methods with Mongoose

#Mongoose







Related Posts

Event Loop 運行機制解析 - Node.js 篇

Event Loop 運行機制解析 - Node.js 篇

DAY43:Adding Big Numbers

DAY43:Adding Big Numbers

【Day 3】Docker Container(容器)與 Volume(數據卷)

【Day 3】Docker Container(容器)與 Volume(數據卷)


Comments