# 前言

這是一篇介紹 Mongoose 基礎語法的文章,內容有新增、修改、刪除,以及資料關聯 .populate 的基礎用法。

# 建立連接 MongoDB

如果 MongoDB 是建立在本地端的話,網址會像下面這樣

await mongoose.connect('mongodb://127.0.0.1:27017/shopApp')
.then(() => {
  console.log("Connection OPEN!!!")
})
.catch((err) => {
  console.log("OH NOOOOO")
  console.log(err)
})

如果是用雲端的話網址會長一點

# 官方文檔

# Schema 與 Model 設計基礎

# 程式碼範例:

const expenseCategorySchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
  }
});
const incomeCategorySchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true,
  }
});
const cashFlowSchema = new mongoose.Schema({
  amount: {
    type: Number,
    min: 0,
    required: true
  },
  type: {
    type: String,
    enum: ['income', 'expense']
  },
  category: {
    type: mongoose.Schema.Types.ObjectId,
    required: true,
    ref: function() {
      return this.type.toString() === 'expense' ? 'ExpenseCategory' : 'IncomeCategory';
    }
  },
  date: {
    type: Date,
    default: Date.now
  },
  description: {
    type: String,
    required: true
  }
});

MongoDB 在定義 Schema 是一個 key 對上一個 value
value 裡面需要指定 type , 其他相關的 required , default , enum , 都是可選的
根據需要加入適當的條件,官網裡有列出所有 validator 及相關的範例這裡只用到了一些

# 補充說明

可以自訂義不符合時的報錯訊息

You can configure the error message for individual validators in your schema. There are two equivalent ways to set the validator error message:

Array syntax: min: [6, 'Must be at least 6, got {VALUE}']
Object syntax: enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }

# 官方文檔

# Schema 導出的部分

const ExpenseCategory = mongoose.model('ExpenseCategory', expenseCategorySchema);
const IncomeCategory = mongoose.model('IncomeCategory', incomeCategorySchema);
const CashFlow = mongoose.model('CashFlow', cashFlowSchema);
module.exports = { ExpenseCategory, IncomeCategory, CashFlow };

mongoose.model('ExpenseCategory', expenseCategorySchema);
程式碼的意思是建立一個名叫 ExpenseCategory 的 model , 參照 expenseCategorySchema 的 Schema

# 插入資料

# 語法

A.insertMany (陣列)
陣列裡面是 Object

# 範例程式:

const makeExpenseCategory = async () => {
  const temp = [
    {
      name: 'Else'
    },
    {
      name: 'Rent'
    },
    {
      name: 'Food'
    },
    {
      name: 'Drink'
    },
    {
      name: 'Bill'
    }
  ]
  await connectDB()
  ExpenseCategory.insertMany(temp)
    .then((p) => { console.log(p) })
    .catch(e => console.log(e))
}
makeExpenseCategory()

這邊先連接了 Database 後再進行插入
如果沒有 await 會有高機率報錯,因為 Database 還沒連接進行插入

# 執行結果

Connection OPEN!!!
[
  {
    name: 'Else',
    _id: new ObjectId('67861bba58428e2f0bba12e7'),
    __v: 0
  },
  {
    name: 'Rent',
    _id: new ObjectId('67861bba58428e2f0bba12e8'),
    __v: 0
  },
  {
    name: 'Food',
    _id: new ObjectId('67861bba58428e2f0bba12e9'),
    __v: 0
  },
  {
    name: 'Drink',
    _id: new ObjectId('67861bba58428e2f0bba12ea'),
    __v: 0
  },
  {
    name: 'Bill',
    _id: new ObjectId('67861bba58428e2f0bba12eb'),
    __v: 0
  }
]

# 官方文檔

# 刪除資料

# 語法

await Character.deleteMany({ name: /Stark/, age: { $gte: 18 } });
// returns {deletedCount: x} where x is the number of documents deleted.

# 範例程式碼:

const deleteExpenseCategory = async () => {
  await connectDB()
  await ExpenseCategory.deleteOne({ name: 'Food' })
    .then(p => { console.log(p)})
  await ExpenseCategory.find()
    .then(p => console.log(p))
}
deleteExpenseCategory()

# 範例輸出

{ acknowledged: true, deletedCount: 1 }
[  
  {
    _id: new ObjectId('67861bba58428e2f0bba12e7'),
    name: 'Else',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12e8'),
    name: 'Rent',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12ea'),
    name: 'Drink',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12eb'),
    name: 'Bill',
    __v: 0
  }
]

# 官方文檔

# 修改資料

# 語法

A.findOneAndUpdate(conditions, update, options)  // returns Query
A.findOneAndUpdate(conditions, update)           // returns Query
A.findOneAndUpdate()                             // returns Query

# 範例程式碼:

const updateExpenseCategory = async () => {
  await connectDB()
  await ExpenseCategory.findOneAndUpdate({ name: 'Bill' }, { name: 'Utility' })
    .then(p => { console.log(p) })
  await ExpenseCategory.find()
    .then(p => console.log(p))
}
updateExpenseCategory()

# 範例輸出

Connection OPEN!!!
{ _id: new ObjectId('67861bba58428e2f0bba12eb'), name: 'Bill', __v: 0 }
[
  {
    _id: new ObjectId('67861bba58428e2f0bba12e7'),
    name: 'Else',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12e8'),
    name: 'Rent',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12ea'),
    name: 'Drink',
    __v: 0
  },
  {
    _id: new ObjectId('67861bba58428e2f0bba12eb'),
    name: 'Utility',
    __v: 0
  }
]

最後還有個參數 Options 這邊沒有多做介紹有興趣可以去官方文檔查看

# 資料關聯與查詢

注意到我 CashFlowSchema 的 category type 是

category: {
  type: mongoose.Schema.Types.ObjectId,
  required: true,
  ref: function () {
    return this.type.toString() === 'expense' ? 'ExpenseCategory' : 'IncomeCategory';
  }
}

ref 的意思就是參照哪一個 collection
這邊注意到我寫了一個 function 來控制要選擇哪一個 collection
這邊不能用 arrow function , arrow function 不會指定 this 而是繼承外部的 this
所以用 function 的 this 會給定當前資料的 this 這樣才能動態選定要哪一個 collection

# 範例程式:

const makeCashFlow = async () => {
  try {
    await connectDB()
    const Food = await ExpenseCategory.findOne({ name: 'Food' })
    const Rent = await ExpenseCategory.findOne({ name: 'Rent' })
    const Drink = await ExpenseCategory.findOne({ name: 'Drink' })
    const Salary = await IncomeCategory.findOne({ name: 'Salary' })
    const Allowance = await IncomeCategory.findOne({ name: 'Allowance'})
    const WindFall = await IncomeCategory.findOne({ name: 'WindFall' })
    // 建立範例 CashFlow 資料
    const cashFlows = [
      // 支出範例
      {
        amount: 500,
        type: 'expense',
        category: Utility,
        date: new Date('2025-01-01'),
        description: 'Month utility'
      },
      {
        amount: 1000,
        type: 'expense',
        category: Rent,
        date: new Date('2025-01-05'),
        description: 'Monthly rent payment'
      },
      {
        amount: 200,
        type: 'expense',
        category: Drink,
        date: new Date('2025-01-10'),
        description: 'Coffee and snacks'
      },
      // 收入範例
      {
        amount: 5000,
        type: 'income',
        category: Salary,
        date: new Date('2025-01-15'),
        description: 'January salary'
      },
      {
        amount: 300,
        type: 'income',
        category: Allowance,
        date: new Date('2025-01-20'),
        description: 'Weekly allowance'
      },
      {
        amount: 800,
        type: 'income',
        category: WindFall,
        description: 'Lottery prize'
      }// 沒有給予 date 自動指定當前時間
    ];
    // 插入資料到資料庫
    CashFlow.insertMany(cashFlows)
      .then((p) => { console.log(p) })
      .catch(e => console.log(e))
  } catch (error) {
    console.error(error);
  }
};
makeCashFlow()

# 範例輸出

Connection OPEN!!!
[
  {
    amount: 500,
    type: 'expense',
    category: new ObjectId('67861bba58428e2f0bba12eb'),
    date: 2025-01-01T00:00:00.000Z,
    description: 'Month utility',
    _id: new ObjectId('67867473d6935a7b89fd3c1e'),     
    __v: 0
  },
  {
    amount: 1000,
    type: 'expense',
    category: new ObjectId('67861bba58428e2f0bba12e8'),
    date: 2025-01-05T00:00:00.000Z,
    description: 'Monthly rent payment',
    _id: new ObjectId('67867473d6935a7b89fd3c1f'),
    __v: 0
  },
  {
    amount: 200,
    type: 'expense',
    category: new ObjectId('67861bba58428e2f0bba12ea'),
    date: 2025-01-10T00:00:00.000Z,
    description: 'Coffee and snacks',
    _id: new ObjectId('67867473d6935a7b89fd3c20'),
    __v: 0
  },
  {
    amount: 5000,
    type: 'income',
    category: new ObjectId('6785ec3ca2ddc9c34522ceca'),
    date: 2025-01-15T00:00:00.000Z,
    description: 'January salary',
    _id: new ObjectId('67867473d6935a7b89fd3c21'),
    __v: 0
  },
  {
    amount: 300,
    type: 'income',
    category: new ObjectId('6785ec3ca2ddc9c34522cecb'),
    date: 2025-01-20T00:00:00.000Z,
    description: 'Weekly allowance',
    _id: new ObjectId('67867473d6935a7b89fd3c22'),
    __v: 0
  },
  {
    amount: 800,
    type: 'income',
    category: new ObjectId('6785ec3ca2ddc9c34522cec9'),
    description: 'Lottery prize',
    _id: new ObjectId('67867473d6935a7b89fd3c23'),
    date: 2025-01-14T14:28:03.474Z,
    __v: 0
  }
]

這邊介紹 .populate 的用法
可以看到上面的範例輸出 category 後面的 value 是一個 ObjectId
我們希望找到對應的 category 裡面的資料,這裡就會用到 .populate

# 範例程式:

const findCashFlow = async () => {
  await connectDB()
  const cashFlows = await CashFlow.find().populate('category','name')
  console.log(cashFlows)
  await CashFlow.find().populate('category').then(e => {
    e.forEach(p => {
      console.log(p.category.name)
    })
  })
}
findCashFlow()

# 範例輸出

Connection OPEN!!!
[
  {
    _id: new ObjectId('67867473d6935a7b89fd3c1e'),
    amount: 500,
    type: 'expense',
    category: { _id: new ObjectId('67861bba58428e2f0bba12eb'), name: 'Utility' },
    date: 2025-01-01T00:00:00.000Z,
    description: 'Month utility',
    __v: 0
  },
  {
    _id: new ObjectId('67867473d6935a7b89fd3c1f'),
    amount: 1000,
    type: 'expense',
    category: { _id: new ObjectId('67861bba58428e2f0bba12e8'), name: 'Rent' },
    date: 2025-01-05T00:00:00.000Z,
    description: 'Monthly rent payment',
    __v: 0
  },
  {
    _id: new ObjectId('67867473d6935a7b89fd3c20'),
    amount: 200,
    type: 'expense',
    category: { _id: new ObjectId('67861bba58428e2f0bba12ea'), name: 'Drink' },
    date: 2025-01-10T00:00:00.000Z,
    description: 'Coffee and snacks',
    __v: 0
  },
  {
    _id: new ObjectId('67867473d6935a7b89fd3c21'),
    amount: 5000,
    type: 'income',
    category: { _id: new ObjectId('6785ec3ca2ddc9c34522ceca'), name: 'Salary' },
    date: 2025-01-15T00:00:00.000Z,
    description: 'January salary',
    __v: 0
  },
  {
    _id: new ObjectId('67867473d6935a7b89fd3c22'),
    amount: 300,
    type: 'income',
    category: {
      _id: new ObjectId('6785ec3ca2ddc9c34522cecb'),
      name: 'Allowance'
    },
    date: 2025-01-20T00:00:00.000Z,
    description: 'Weekly allowance',
    __v: 0
  },
  {
    _id: new ObjectId('67867473d6935a7b89fd3c23'),
    amount: 800,
    type: 'income',
    category: { _id: new ObjectId('6785ec3ca2ddc9c34522cec9'), name: 'WindFall' },
    description: 'Lottery prize',
    date: 2025-01-14T14:28:03.474Z,
    __v: 0
  }
]
Utility
Rent
Drink
Salary
Allowance
WindFall

.populate 還有很多有用的功能
詳細用法可以到官方文檔查看