mongoose基礎使用

Shapeying發表於2021-08-04

mongoose與mongodb

首先,要明確mongoosemongodb是什麼?

mongodb是一種文件資料庫;而mongoose是一種能在node環境中優雅地操作mongodb的物件模型工具庫,它提供了型別轉換、驗證、查詢等等各種便捷能力。

其次,要了解mongoosemongodb的一些基礎概念,及其之間的聯絡。

mongodb中的基礎概念

mongodb將資料記錄儲存為文件(documents),這些文件會收集在集合(collections)中,而一個資料庫(database)會儲存一個或者多個集合,如下圖:

可以看到,資料是以一個個document的形式儲存的。

mongoose中的基礎概念

mongoose作為操作mongodb的工具庫,可以理解為就是在操作documents。它入門的概念是Schema ,是用來定義collections中的documents的形狀;通過Schema可以生成一個建構函式Models,它對應的就是collections,而它的例項也稱為Documents,對應的就是mongodb中的documents

執行Documents的相關 Api 就能把資料寫到mongodbdatabase中。

    // 建立Schema,描述文件的形狀
    const personSchema = new Schema({
      name: String,
      age: Number,
      address: String,
    });

    // 建立Model,對應的是database中的 persons集合
    const Person = model('Person', personSchema);

    // 生成document,內容要和定義的Schema保持一致
    const person = new Person({
      name: 'zhang',
      age: 17,
      address: 'hubei',
    });

    // 儲存此文件到mongodb
    await person.save();

同時Models提供了一些CRUD輔助函式,這些輔助函式能便捷地進行增刪改查操作,比如Model.find({}),它們會返回Query,可以通過傳入一個規則物件,或者鏈式呼叫來組合一組操作。然後觸發執行,之後就會在mongodb中執行對應的操作,觸發執行有多種方式

    // 觸發方式一,直接傳入callback
    // 或者先建立Query物件,然後通過 .exec() 傳入callback 觸發執行

    Person.find(
      // 查詢規則
      {
        age: {
          $gt: 17,
        },
      }, function(err, person) {
        if (err) return console.log(err);

        console.log(person);
      });

    // 觸發查詢方式二 觸發 .then()

    // 傳入查詢規則,query為一個 Query物件
    const query = Person.find({
      age: {
        $gt: 17,
      },
    });

    // 通過await 觸發 .then()
    const doc = await query;

    console.log(doc);

    // 都會列印輸出
    [
      {
        _id: 6102651d7ac5ce4f8cff5c0d,
        name: 'lei',
        age: 18,
        address: 'hubei',
        __v: 0
      }
    ]
  }

總之,增刪改查都是從Model著手,通過相關API建立對應操作,然後觸發操作執行,實際寫入到mongodb

mongoose常用語法

這裡記錄一些 mongoose 常用語法

連線資料庫

mongoose.connect

mongoose.connect建立預設連線,即 mongoose.connection ,使用mongoose.model建立模型時也是預設使用此連線。

mongoose.connect('mongodb://username:password@host:port/database?options', [, options]);options詳情,在建立與mongodb連線的過程中,會發出多種事件

連線副本集時,傳入地址列表 mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]);,還可以指定副本集名稱選項(replicaSet)。

/*testMongoose.js*/
'use strict';
const mongoose = require('mongoose');

// 監聽事件
mongoose.connection.on('connecting', () => {
  console.log('mongoose 開始進行連線');
});

mongoose.connection.on('connected', () => {
  console.log('mongoose 連線成功');
});

mongoose.connection.on('error', err => {
  console.log('mongoose connnect失敗', err);
});

// 建立副本集連線
mongoose.connect('mongodb://url1:24000,url2:24000,url3:24000/myDatabase?replicaSet=myReplicaSet',
  {
    useNewUrlParser: true,
    authSource: 'admin',
    useFindAndModify: false,
    useUnifiedTopology: true,
  }
);


/*demo.js*/
const mongoose = require('mongoose');

const { model } = mongoose;

// 使用預設連線建立模型
const Person = model('Person', personSchema);

mongoose.createConnection

當需要連線到多個資料庫時,可以使用mongoose.createConnection(),它的引數和mongoose.connect()一樣,會返回一個Connection物件,注意要保留對此物件的引用,以便用它來建立Model

/*testMongoose.js*/
// 建立連線
const conn = mongoose.createConnection(
  'mongodb://url1:24000,url2:24000,url3:24000/myDatabase?replicaSet=myReplicaSet',
  {
    useNewUrlParser: true,
    authSource: 'admin',
    useFindAndModify: false,
    useUnifiedTopology: true,
  }
);

conn.on('connected', () => {
  console.log('mongoose 連線成功');
});

// 匯出connection物件
module.exports = conn;


/*demo.js */
const conn = require('../utils/testMongoose');
// 使用指定的connection物件建立連線
const Person = conn.model('Person', personSchema);

const person = new Person({
  name: 'qian',
  age: 31,
  address: 'beijing',
});

// 儲存此文件到mongodbs
await person.save();

定義Schema

const schema = new Schema({...}, options);

mongoose中的所有操作都是從定義Schema開始的,mongoose提供了豐富的屬性來定義Schema,可以指定型別,是否必須,校驗規則,是否自動轉換,索引方式等等。除此之外,還可以給Schema定義各種方法、虛擬屬性、別名等等,可以輔助查詢、轉換資料,在定義Schema時還有多種配置項

還提供了增加定義 schema.add({...})移除定義 schema.remove(Name) 等等API

另外,還可以通過Schema定義中介軟體,在函式執行過程中,會觸發對應的中介軟體,多用於編寫外掛。

const mongoose = require('mongoose');

const { Schema } = mongoose;
// 建立Schema,描述文件的形狀
const personSchema = new Schema({
  n: {
    type: String, // 型別
    required: true, // 校驗規則 - 必須
    alias: 'name', // 別名 資料庫中存放的是 n, 但是在程式中可以使用name來訪問和賦值,但是find查詢時不能使用別名
    lowercase: true, // 自動轉換為全小寫
  },
  age: {
    type: Number, 
    default: 18, // 預設值
    min: [ 10, '年齡不能小於10' ], // 校驗規則
    validate: { // 自定義校驗方法
      validator(v) {
        return v <= 100;
      },
      message: '{VALUE} 必須小於等於100',
    },
  },
  address: {
    type: String,
    enum: { // 校驗規則
      values: [ 'hubei', 'guangzhou' ],
      message: '{VALUE} is not supported',
    },
  },
}, {
  toObject: { // 屬性配置 - 轉換成物件時會被呼叫
    virtuals: true, // 允許虛擬屬性
    transform(doc, ret) { // 對返回物件做處理
      ret.id = ret._id;

      delete ret._id;
      delete ret.__v;
    },
  },
});

personSchema.virtual('Age').get(function() { // 定義虛擬屬性
  return this.age + '歲'; 
});

personSchema.statics.findByAge = function(num) { // 定義靜態函式,可以封裝一些便捷功能
  return this.find({
    age: num,
  });
};

// 定義中介軟體
personSchema.post('validate', function(doc) {
  console.log('%s has been validated (but not saved yet)', doc._id);
});

通過以下例子,說明上面配置的作用

// 不符合規則的資料
const person1 = new Person({
  name: 'Test',
  age: 9,
  address: 'beijing',
});

// 資料儲存時會根據Schema規則進行校驗
await person1.save();
// 丟擲錯誤 nodejs.ValidationError: Person validation failed: age: 年齡不能小於10, address: beijing is not supported

// 符合規則的資料
const person2 = new Person({
  name: 'TestLei',
  age: 16,
  address: 'hubei',
});

// 資料儲存時會根據Schema規則進行校驗
await person2.save();

// 觸發中介軟體  61090d88a848e3acf4113dda has been validated (but not saved yet)

console.log(person);
// {
//   age: 16,
//   n: 'testlei',    -> 自動進行小寫轉換
//   address: 'hubei',
//   name: 'testlei', -> 別名 注意此處是因為是在toObject中進行了相關配置
//   Age: '16歲',  -> 虛擬屬性 注意此處是因為是在toObject中進行了相關配置
//   id: 61090d88a848e3acf4113dda -> toObject進行的資料處理
// }
    // 使用自定義的方法進行查詢
    const p1 = await Person.findByAge(16);

    console.log(p1);   
    // [
    //   {
    //     age: 16,
    //     n: 'testlei',
    //     address: 'hubei',
    //     name: 'testlei',
    //     Age: '16歲',
    //     id: 61090d88a848e3acf4113dda
    //   }
    // ]

建立Model

定義好Schema之後,就可以用來建立對應的Model

model('CollectionName', Schema)

mongoose會使用第一個引數的全小寫、複數格式到mongodb中找collection(eg: collectionnames)

在連線資料庫的時候,已經有建立Model的示例,需要注意的就是,使用mongoose.model()建立時使用的是預設連線,額外建立的連線,需要使用對應的Connection.model()

  // 使用預設連線建立模型
  const Person = model('Person', personSchema);

  const conn = mongoose.createConnection({...});
  // 使用指定的connection物件建立連線
  const Person = conn.model('Person', personSchema);

增刪改查

新增

通過建構函式Model生成例項(Document)

    // 通過 Model 建立 Document 
    // https://mongoosejs.com/docs/api/model.html#model_Model
    const person = new Person({
      name: 'TestLei',
      age: 16,
      address: 'hubei',
    });

    // 寫入到database中
    person.save();

可一次新增多條資料

    // 新增一條資料
    await Person.create({
      name: 'zhao',
      age: 16,
      address: 'hubei',
    });

    // 新增多條資料
    await Person.create([
      {
        name: 'qian',
        age: 17,
        address: 'hubei',
      },
      {
        name: 'qian',
        age: 18,
        address: 'hubei',
      },
    ]);

此方法新增多條資料比create效率更高

  await Person.insertMany([
    {
      name: 'zhou',
      age: 17,
      address: 'hubei',
    },
    {
      name: 'zhou',
      age: 18,
      address: 'hubei',
    },
  ]);

查詢

Model.find

Model.find( [過濾規則] , [返回欄位]) , [配置項] , callback)

返回欄位 可以指定需要返回哪些欄位,或者指定不需要哪些欄位

配置項可以限制返回條數,排序規則,跳過文件數量(分頁)等等。

find中傳入的所有引數都有對應的工具函式,而且Model.find返回的是一個Query物件,Query原型上的工具函式都是返回this,所以可以鏈式呼叫

以上兩種思路是等價的


    const p1 = await Person.find({
      age: {
        $gte: 12, // age大於等於12
      },
      n: {
        $in: [ 'zhao', 'qian' ], // n是[ 'zhao', 'qian' ]中的一個
      },
    }, 
    'n age -_id', // 返回 n age欄位,不返回 _id
    { 
      sort: { // 按age降序排序
        age: -1,
      },
      limit: 2, // 只返回兩條資料
    });

    console.log(p1);
    // [ { age: 18, n: 'qian' }, { age: 17, n: 'qian' } ]

    // 以下是通過工具函式的等價寫法

    const p2 = await Person
      .find({})
      .gte('age', 12)
      .where('n')
      .in([ 'zhao', 'qian' ])
      .select('n age -_id')
      .limit(2)
      .sort({
        age: -1,
      });

    console.log(p2);
    // [ { age: 18, n: 'qian' }, { age: 17, n: 'qian' } ]

查詢常用的過濾規則及對應的工具函式如下:

工具函式 過濾操作符 含義 使用方式
eq() $eq 與指定值相等 { <field>: { $eq: <value> } }
ne() $ne 與指定值不相等 { <field>: { $ne: <value> } }
gt() $gt 大於指定值 {field: {$gt: value} }
gte() $gte 大於等於指定值 {field: {$gte: value} }
lt() $lt 小於指定值 {field: {$lt: value} }
lte() $lte 小於等於指定值 {field: {$lte: value} }
in() $in 與查詢陣列中指定的值中的任何一個匹配 { field: { $in: [<value1>, <value2>, ... <valueN> ] } }
nin() $nin 與查詢陣列中指定的值中的任何一個都不匹配 { field: { $nin: [ <value1>, <value2> ... <valueN> ]} }
and() $and 滿足陣列中指定的所有條件 { $and: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] }
nor() $nor 不滿足陣列中指定的所有條件 { $nor: [ { <expression1> }, { <expression2> }, ... { <expressionN> } ] }
or() $or 滿足陣列中指定的條件的其中一個 { $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }
not() $not 反轉查詢,返回不滿足指定條件的文件 { field: { $not: { <operator-expression> } } }
regex() $regex 可以被指定正則匹配 { <field>: { $regex: /pattern/, $options: '<options>' } } { <field>: { $regex: 'pattern', $options: '<options>' } } { <field>: { $regex: /pattern/<options> } }
exists() $exists 匹配存在指定欄位的文件 { field: { $exists: <boolean> } }
type() $type 返回欄位屬於指定型別的文件 { field: { $type: <BSON type> } }
size() $size 陣列欄位的長度與指定值一致 { <field>: { $size: <value> } }
all() $all 陣列中包含所有的指定值 { <field>: { $all: [ <value1> , <value2> ... ] } }
Model.findOne | Model.findById()

findOne的使用方式和find一樣,適用於只查詢一條資料

    const p3 = await Person.findOne({
      age: {
        $gte: 12,
      },
      n: {
        $in: [ 'zhao', 'qian' ],
      },
    }, 'n age -_id', {
      sort: {
        age: -1,
      },
    });

    console.log(p3);
    // { age: 18, n: 'qian' }

如果過濾條件是 _id,可以使用 findById

    const p4 = await Person.findById('61090d4287e3a9a69c50c842', 'n age -_id');

更新

更新資料有兩種思路:

  • 查詢資料,然後修改,再通過save儲存
  • 使用 update系列 API

第一種思路寫法複雜,效率不高,但是會觸發完整的校驗和中介軟體;


    // 查詢
    const p4 = await Person.findById('61090d4287e3a9a69c50c842');

    // 賦值
    // 不符合ShameType的資料
    p4.address = 'guizhou';

    // 儲存
    await p4.save();

    // 校驗報錯
    // Person validation failed: address: guizhou is not supported

第二種 寫法預設不會觸發校驗(通過配置項可以設定校驗),只會觸發特定的中介軟體;

    // 沒有觸發校驗
    wait Person.updateOne({
      _id: ObjectId('61090d4287e3a9a69c50c842'),
    }, {
      address: 'guizhou',
    });

    // 增加配置項 {runValidators: true,} 可觸發校驗

update系列的方法主要有

updateOneupdateMany的使用方式基本一致,只是一個只會更新第一條資料,一個會更新所有符合條件的資料。

updateXXX([過濾條件],[更新資料],[配置項],[callback])

過濾條件find的規則一樣

更新資料預設為$set操作符,即更新傳入的欄位,其他的操作符和mongodb保持一致,檢視詳情

配置項可配置是否進行校驗,是否進行資料覆蓋,是否能批量更新等等,不同的方法稍有不同,詳見每個API的文件

findByIdAndUpdatefindOneAndUpdate 主要是會返回查詢到的資料(更新之前的)。

    const a = await Person.findByIdAndUpdate({
      _id: '61090d4287e3a9a69c50c842',
    }, {
      address: 'hubei',
    });

    console.log(a);
  // {
  //   age: 16,
  //   _id: 61090d4287e3a9a69c50c842,
  //   n: 'testlei',
  //   address: 'guizhou', // 更新之前的資料
  //   __v: 0
  // }

    // 增加 {overwrite: true} 配置可進行資料覆蓋

刪除

remove系列的方法主要有

findOneAndRemove(),Model.findByIdAndDelete() 除了會刪除對應的資料,還會返回查詢結果。

    const a = await Person.remove({
      _id: ObjectId('61090d4287e3a9a69c50c842'),
    });

    console.log(a.deletedCount); 
    // 1    

    // 刪除Persons集合的所有資料
    await Person.remove({});

    
    const a = await Person.findOneAndRemove({
      n: 'zhao',
    });

    console.log(a);
    // {
    //   age: 16,
    //   _id: 6109121467d113aa2c3f4464,
    //   n: 'zhao',
    //   address: 'hubei',
    //   __v: 0
    // }

表填充

mongoose還提供了一個便捷能力,可以在文件中引用其他集合的文件,稱之為Populate

const workerSchema = new Schema({
  job: String,
  person: { // person欄位,引用Persons表中的文件,通過 _id 進行關聯
    type: Schema.Types.ObjectId,
    ref: 'Person', // 指定集合名稱
  },
  workYear: Number,
});

const Worker = model('Worker', workerSchema);

在建立文件時,需要寫入所關聯資料的 _id


  const person = new Person({
    name: 'lei',
    age: 28,
    address: 'hubei',
  });

  await person.save();

  const worker = await new Worker({
    job: 'banzhuan',
    workYear: 6,
    person: person._id, // 寫入_id
  });

  await worker.save();

  console.log(worker);
  // {
  //   _id: 610a85c10aec8ad374de9c29,
  //   job: 'banzhuan',
  //   workYear: 6,
  //   person: 610a85c00aec8ad374de9c28, // 對應person文件的 _id
  //   __v: 0
  // }

使用 Query.prototype.populate(),就可以在查詢資料時,便捷地取到所填充文件的資料。還可以通過配置,對關聯文件進行過濾,指定返回欄位,排序規則等等。

    const a = await Worker.find({
      job: 'banzhuan',
    }).populate('person');

    // [
    //   {
    //     _id: 610a85c10aec8ad374de9c29,
    //     job: 'banzhuan',
    //     workYear: 6,
    //     person: { // Persons中文件的資料
    //       age: 28,
    //       _id: 610a85c00aec8ad374de9c28,
    //       n: 'lei',
    //       address: 'hubei',
    //       __v: 0
    //     },
    //     __v: 0
    //   }
    // ]

    const b = await Worker.find({
      job: 'banzhuan',
    }).populate({
      path: 'person', // 指定路徑,即欄位名
      match: { age: { $gte: 28 } }, // 對填充文件的過濾條件,和find的過濾規則一致
      select: 'age n -_id', // 指定需要返回的欄位,和find的寫法一致
    });

    // [
    //   {
    //     _id: 610a85c10aec8ad374de9c29,
    //     job: 'banzhuan',
    //     workYear: 6,
    //     person: { age: 28, n: 'lei' },
    //     __v: 0
    //   }
    // ]