跳至主要内容

[Mongo] Mongoose 操作

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

安裝

$ npm install --save mongoose

基本使用

與 MongoDB 建立連線

// ./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 的資料會包含哪些型態。
// ./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

// ./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 後可使用的方法:

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

CREATE

一次建立一個 document

/**
* 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 的效能會更好
/**
* 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

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

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

READ

/**
* 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 的寫法:

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

其他可用的 query 方法:

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

UPDATE

// 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:

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

Model.remove()

Schema

建立 Schema 時可用的其他項目

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

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 前可以介入的時間點:

/**
* 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 添加可用的方法:

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 來呼叫這個方法:

/**
* 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 建立連線

// ./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 的資料夾中:

// ./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

// ./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');
});
});

參考