[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)],
);
- Queries @ Mongoose Guide
 - Queries @ Mongoose API
 - Collection Methods @ MongoDB
 
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.remove
移除 collection 中符合條件的所有 documents:
const res = await Todo.remove({ completed: true });
res.deletedCount; // Number of documents removed
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');
  });
});