PJCHENder 未整理筆記

[Mongo] Mongoose 操作

2018-12-09

[Mongo] Mongoose 操作

@(Database)[Mongo, NoSQL, cli, mongoose]

  • Mongoose 會追蹤在與 DB 連線前對資料庫進行的請求,並在連線後加以執行。
  • Mongoose 是 MongoDB 的 ODM(Object Data Modeling) 套件,可以讓我們更方便處理 CRUD。
  • 透過 mongoose 的使用,我們可以更像在操作 relational database。

安裝

1
$ npm install --save mongoose

基本使用

與 MongoDB 建立連線

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ./app.js
const mongoose = require('mongoose');

/**
* 與 mongoDB 建立連連
* mongoose.connect('mongodb://[資料庫帳號]:[資料庫密碼]@[MongoDB位置]:[port]/[資料庫名稱]')
* mongoDB 預設的 port 是 27017,這裡可以省略
* todo 是 database 的名稱,當 app 執行時,mongoose 會自動建立這個 database
*/

mongoose.connect('mongodb://localhost:27017/todo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})

// 取得資料庫連線狀態
const db = mongoose.connection;
db.on('error', err => console.error('connection error', err)); // 連線異常
db.once('open', db => console.log('Connected to MongoDB')); // 連線成功

建立 Schema(資料庫綱要)

  • schema 是用 JSON 的方式來告訴 mongo 說 document 的資料會包含哪些型態。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ./models/Animal.js
const mongoose = require('mongoose');

let AnimalSchema = new mongoose.Schema({
size: String,
mass: Number,
category: {
type: String,
default: 'on land'
},
name: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});

// 添加 instance method (給 document 用的方法)
AnimalSchema.methods.getCategory = function() {
// 這裡的 this 指透過這個 function constructor 建立的物件
console.log(`This animal is belongs to ${this.category}`);
};

// 添加 instance method 的另一種寫法
AnimalSchema.methods('getName', function() {
console.log(`The animal is ${this.name}`);
});

// Compile Schema 變成 Model,如此可以透過這個 Model 建立和儲存 document
// 會在 mongo 中建立名為 animals 的 collection
module.exports = mongoose.model('Animal', AnimalSchema);

使用建立好的 Model 來新增 Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ./app.js
const Animal = require('./models/Animal');

// 建立 document
const elephant = new Animal({
category: 'on land',
mass: 6000,
size: 'big',
name: 'Lawrence'
});

// 直接存取 elephant 這個 instance 的 type
console.log(elephant.category); // "on land"

// 透過在 Model 中定義的 instance methods 取得 elephant 的 category
elephant.getCategory(); // "This animal is belongs to on land"

// 儲存 document
// 透過
elephant.save((err, animal) => {
if (err) {
return console.error(err);
}
console.log('document saved');
db.close(); // 結束與 database 的連線
});

Model

這裡都是 Schema 已經 compile 成 Model 後可使用的方法:

1
const Animal = mongoose.model('Animal', AnimalSchema);

CREATE

一次建立一個 document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* document.save(callback(err, document))
**/
const elephant = new Animal({
type: 'elephant',
size: 'big',
color: 'gray',
mass: 6000,
name: 'Lawrence'
});

elephant.save((err, elephant) => {
if (err) {
return console.error(err);
}
// 第二個參數 elephant 指的是儲存好的 document
});

一次建立多個 document

keywords: create, insertMany
  • 可以在 create 後帶入陣列亦可新增多筆 documents,但使用 insertMany 的效能會更好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Model.insertMany(dataToCreate, callback(err, documents))
* 第二個參數指的是透過 create 建立起來的 documents
*/
const animalData = [
{
type: 'mouse',
color: 'gray',
mass: 0.034,
name: 'Marvin'
},
{
type: 'nutria',
color: 'brown',
mass: 6.35,
name: 'Gretchen'
},
{
type: 'wolf',
color: 'gray',
mass: 45,
name: 'Iris'
}
];

Animal.insertMany(animalData, (err, animals) => {
if (err) {
return console.error(err);
}
});

DELETE

1
2
3
4
5
/**
* collection.deleteOne(conditions, options, callback)
*/
await Animal.deleteMany(); // 刪除 Animal 中的所有 documents
await Animal.deleteMany({ color: 'red' })

Model.deleteMany(), Model.deleteOne() @ mongoose

READ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Model.find(condition, projection, options, callback(err, documents))
* 回傳 "query"
*/

// 回傳 Animal 中的所有 documents
Animal.find({}, (err, animals) => {
if (err) {
return console.error(err);
}
console.log(animals);
});

// 透過 query 尋找特定的 documents
Animal.find({ size: 'big' }, (err, animals) => {
if (err) {
return console.error(err);
}
animals.forEach(animal => {
console.log(`${animal.name}: ${animal.type}`);
});
});

// 使用正規式尋找
Animal.find({ name: /^fluff/ }, callback(err, documents));

/**
* 進階用法
*/
Animal.find({}, null, { sort: { createdAt: -1 } }, (err, animals) => {
// -1 表示 descending orders
});

Animal.find({})
.sort({ createdAt: -1 })
.exec(function(err, animals) {
if (err) {
return console.error(err);
}
res.json(animals);
});

使用 async … await 的寫法:

1
2
3
4
5
const getTodos = async () => {
const query = Todo.find();
const documents = await query.exec(); // query.exec return a promise
console.log('documents', documents);
}

其他可用的 query 方法:

1
2
Model.findById(id, [projection], [options], [callback(err, documents)])
Model.findOne([questionCondition], [projection], [options], [callback(err, doc)])

UPDATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Model.prototype.save()
const todo = Todo({
name: req.body.name, // name 是從 new 頁面 form 傳過來
});

// 使用 callback
todo.save((err) => {
if (err) return console.error(err);
return res.redirect('/'); // 新增完成後,將使用者導回首頁
});

// 使用 async
const saveTodo = async () => {
await todo.save();
return res.redirect('/'); // 新增完成後,將使用者導回首頁
}

Model.prototype.save()

Model.remove

移除 collection 中符合條件的所有 documents:

1
2
const res = await Todo.remove({ completed: true });
res.deletedCount; // Number of documents removed

Model.remove()

Schema

建立 Schema 時可用的其他項目

1
2
3
4
5
6
7
8
9
10
11
12
13
const elephant = new Animal({
name: {
type: String,
default: 'Angela', // 預設值
required: true, // 表示為必填欄位,若缺少此欄位,mongoDB 不會建立此 document 並會回傳 error
trim: true, // 去除掉不必要的空白
unique: true // 確認這個 email 的值沒有在其他 document 中出現過(也就是沒有相同的 email)
},
createdAt: {
type: Date,
default: Date.now
}
})

嵌套式的 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mongoose = require('mongoose');

const AnswerSchema = new mongoose.Schema({
text: String,
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
votes: { type: Number, default: 0 }
});

const QuestionSchema = new mongoose.Schema({
text: String,
createdAt: { type: Date, default: Date.now },
answers: [AnswerSchema] // 告訴 mongoose answers 會 nested in AnswerSchema
});

為 Schema 增加 hook method

hook method 是在 document 要寫入或修改 db 前可以介入的時間點:

1
2
3
4
5
6
7
8
9
10
11
/**
* Schema.pre('save', callback[next])
**/
const mongoose = require('mongoose');
const AnimalSchema = new mongoose.Schema();

// 不可用 arrow function
AnimalSchema.pre('save', function(next) {
console.log(this); // 這裡的 this 會指稱到被儲存的 document 物件
next();
});

Middleware @ Mongoose

在 Schema 中添加 instance methods

透過添加 instance methods,可以讓每一個建立出來的實例 (instance),即,document 添加可用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mongoose = require('mongoose');
const AnimalSchema = new mongoose.Schema();

/**
* Schema.methods.methodName = function([arg], [callback]) {...}
**/
// 要寫在 Schema 被 Compile 成 Model 之前定義
AnimalSchema.methods.findSameColor = function(callback) {
// 這裡的 this 指稱的是透過這個 Schema 所建立的物件
return this.model('Animal').find({ color: this.color }, callback);
};

/**
* 另一種寫法
* Schema.method('methodName', function([arg], [callback]){...})
**/
AnimalSchema.method('update', function(updates, callback) {
// 這裡的 this 指稱的是透過這個 Schema 所建立的物件
this.parent().save(callback); // 可以儲存此物件
});

在 Schema 中增加 statics method (class method)

建立的 Class Method 可以讓 Schema compile 成 Model 後,直接透過 Model 來呼叫這個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Schema.statistics.methodName = function([arg], [callback]){ ... }
*/

const mongoose = require('mongoose');
const AnimalSchema = new mongoose.Schema();

AnimalSchema.statics.findSize = function(size, callback) {
// 這裡的 this 指該 collection (Animal)
this.find({ size: size }, callback);
};
const Animal = mongoose.model('Animal', AnimalSchema);

// 在 Controller 中可以使用
Animal.findSize('small', function() {
/*...*/
});

在 Express 中使用 Mongoose

與 MongoDB 建立連線

1
2
3
4
5
6
7
8
9
10
11
12
13
// ./app.js
const express = require('express');
const mongoose = require('mongoose');
const app = express();

mongoose.connect('mongodb://localhost:27017/todo');
const db = mongoose.connection;
db.on('error', err => {
console.error(err);
});
db.once('open', db => {
console.log('Connected to MongoDB');
});

建立 Schemas

把 Schema (Model) 放在 models 的資料夾中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ./models/user.js

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true, // 必填欄位,若缺少此欄位,mongoDB 不會建立此 document 並會回傳 error
trim: true, // 去除掉不必要的空白
unique: true // 確認這個 email 是唯一
},
name: {
type: String,
required: true,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});

module.exports = mongoose.model('User', UserSchema);

建立 Controllers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ./routes/index

const User = require('./models/user');

router.post('/register', (req, res, next) => {
// 確認沒有漏填欄位
const noEmptyData =
req.body.email &&
req.body.name &&
req.body.favoriteBook &&
req.body.password &&
req.body.confirmPassword;

// 確認第一次和第二次輸入的密碼相同
const validConfirmPassword = req.body.password === req.body.confirmPassword;
if (!noEmptyData) {
const err = new Error('Some fields are empty');
err.status = 400;
return next(err);
}

if (!validConfirmPassword) {
const err = new Error('Passwords do not match');
err.status = 400;
return next(err);
}

// 資料無誤,將使用者填寫的內容存成物件
const userData = {
email: req.body.email,
name: req.body.name,
favoriteBook: req.body.favoriteBook,
password: req.body.password
};

// 使用 Create 將資料寫入 DB
User.create(userData, (err, user) => {
if (err) {
return next(err);
}

return res.redirect('/profile');
});
});

參考

掃描二維條碼,分享此文章