前端轉全棧必看的 MongoDB 入門

MongoDB 是一個存儲文檔的非關係型數據庫。

數據庫、集合和文檔

開始使用 MongoDB 之前有必要先搞清楚幾個概念,數據庫、集合和文檔。

它們之間的關係是這樣的,一個 MongoDB 服務器可以有多個數據庫,一個數據庫下可以有多個集合,一個集合下有多個文檔。

圖片

集合和文檔分別對應傳統關係型數據庫裏的表(table)和行(row)。

圖片

下面例子就是一個文檔,看着是不是和 JSON 很像。

{
    "_id" : ObjectId("62a7fd9fbd3e6ff14d853d19"),
    "title" : "MongoDB 入門指南",
    "content": "學習 MongoDB 基本概念",
    "comments" : [
        {
            "content" : "評論1",
            "author" : "name1",
            "vote" : 1
        },
        {
            "content" : "評論2",
            "author" : "name2",
            "vote" : 2
        },
    ]
}

MongoDB 裏的文檔是 BSON 格式,所謂的 BSON,實際上是 JSON 的超集(好比如 TypeScript 是 JavaScript 的超集一樣)它和 JSON 相比能保存更多的格式,例如 Date、二進制數據。一個文檔可以隨意增加或刪除它的字段,並且同一集合下的文檔也可以互不相同,這就給了開發者很大的靈活性,這也是 MongoDB 的一個優勢。

ObjectId 和 _id

剛開始接觸 MongoDB 時,一般都會對 ObjectId 和 _id 產生些許疑惑。並且這 2 個概念也是項目裏面經常需要用到的,因此有必要事先搞清楚它們。

集合中的每個文檔都必須要有一個 _id 字段,用來識別每個文檔的唯一性。_id 的值可以是任意類型,但默認爲 ObjectId 類型。

在 MongoDB 裏面可以手動生成一個 ObjectId 類型的值。

// 事實上 ObjectId 是一個類,從 bson 這個模塊導出來的
import { ObjectId } from 'mongodb'
// 不帶參數
const objectId = new ObjectId() // ObjectId("62a6df547c2aa16d4c0d6553")
// 帶參數
const objectId = new ObjectId("62a6df547c2aa16d4c0d6553") // // ObjectId("62a6df547c2aa16d4c0d6553")
// 把 ObjectId 類型轉成字符串
new ObjectId().toString() // 62a6df547c2aa16d4c0d6553

做項目的時候,經常需要把 ObjectId 類型轉成字符串返回給客戶端;同樣的客戶端傳一個字符串到服務端,需要先轉成 ObjectId 類型再到數據庫裏匹配查找。

圖片

這顯得有點麻煩,不過好在使用 TSRPC 框架不用擔心這個問題,定義協議時可以直接使用 ObjectId 類型,後面講到項目實戰時會說明這一點。

另外, ObjectId 中會包含文檔創建時的時間戳。

MongoDB Shell 和 MongoDB 驅動

我們安裝 MongoDB 時會附帶一個 MongoDb Shell,就好比如安裝 Node ,會附帶一個 REPL(交互式解釋器)。在 MongoDB Shell 裏面可以執行數據庫相關的操作,但是如果我們要在應用程序裏面使用 MongoDB 的話,需要安裝對應的驅動。下面這些語言都有對應的驅動程序。

圖片

不管是 Shell 還是驅動,其實它們的內部都是調 MongoDB 服務完成對數據的操作。

圖片

CRUD 操作

不管我們使用的是關係型數據庫如 MySQL,還是非關係型數據庫 MongoDB,都離不開對數據的增刪查改這 4 個基本的操作。

下面分別來介紹這 4 種操作。

對比 localStorage 的增查刪

其實 MongoDB 裏面的增刪改查操作和瀏覽器裏面的 localStorage,操作大致上是類似的,只不過存數據的地方不一樣,還有就是 MongoDB 提供的方法更多一些且更靈活一些。

// 增
localStorage.setItem('name', 'xiedun');
// 查
localStorage.getItem('name')
// 刪
localStorage.removeItem('name')

Create 創建文檔

往集合中插入文檔

insertOne()

insertOne 方法往集合裏插入一個文檔

db.user.insertOne({ name: 'xiedun' })

在插入時,可以自己指定 _id 的值,如果不傳,系統默認會幫我們生成一個 ObjectId 類型的值。有一點要注意的是,如果自己指定 _id 的值,那麼要確保在同一個集合裏面不會重複,否則會拋出一個 WriteError 。

db.user.insertOne({ _id: 111, name: 'xiedun' })

insertMany()

當有多個文檔要一起插入時,可以使用 insertMany 這個方法。使用該方法有 2 點需要注意,一就是,插入的文檔最大爲 48M 的數據,如果超過的話,驅動程序會自己進行切分再插入。

db.user.insertMany([
  {name: 'name1'}, 
  {name: 'name2'},
  {name: 'name3'},
  {name: 'name4'},
  {name: 'name5'},
])

第二點是該方法的第二個參數 ordered ,表示插入時文檔是否按照順序插入,默認爲 true,即按照順序插入。在該值下插入某個文檔遇到錯誤時,後面的文檔將不會繼續插入。如果值爲 false,則只是當前錯誤的文檔不會插入,其它文檔會以亂序的方式插入。

db.user.insertMany([
  {_id: 1, name: 'name1'}, 
  {_id: 2, name: 'name2'},
  {_id: 3, name: 'name3'},
  {_id: 3, name: 'name4'},
  {_id: 5, name: 'name5'},
], { ordered: false })

Delete 刪除文檔

刪除集合中的文檔

deleteOne()

該方法刪除集合中的一個文檔,當有多個文檔滿足條件時,它會刪除滿足條件的第一個文檔。

db.user.deleteOne({_id: 1})

deleteMany()

該方法與 deleteOne 不一樣的是,它會刪除滿足條件的所有文檔。

db.user.deleteMany({type: '1'})

當我們需要刪除一個集合下的所有文檔時,可以使用 deleteMany 方法,傳入一個空的參數 db.user.deleteMany({}),除此之外還有另外一個更快的方法:db.user.drop()

Update 更新文檔

我們知道了如何創建文檔,但如果要更新一個文檔,要怎麼做?

updateOne()

更新運算符 $set $inc $push

此方法主要需要傳 2 個參數,第一個是過濾文檔,找出需要更改的文檔;第二個參數是更新運算符,例如:$set $inc $push

db.user.updateOne(
  { name: 'xiedun' }, 
  { $set: {name: '謝頓'} }
)

我們可以在第三個參數傳一些選項,例如 upsert 表示如果找不到該文檔,則會創建它。

db.user.updateOne(
  {name: 'xiedun1'}, 
  {$set: {name: '謝頓1'}}, 
  {upsert: true}
)

更新文檔,還有一些深入的操作,例如針對下面這個文檔,我們想要更新某一條評論的投票數。

{
    "_id" : ObjectId("62a710c1bd3e6ff14d853d17"),
    "title" : "mongodb學習",
    "comments" : [
        {
            "content" : "評論1",
            "author" : "name1",
            "vote" : 1
        },
        {
            "content" : "評論2",
            "author" : "name2",
            "vote" : 3
        },
        {
            "content" : "評論3",
            "author" : "name3",
            "vote" : 3
        },
        {
            "content" : "評論4",
            "author" : "name4",
            "vote" : 4
        },
        {
            "content" : "評論5",
            "author" : "name5",
            "vote" : 5
        }
    ]
}

如果我們明確知道這條評論所在的位置,可以這樣做。

db.posts.updateOne(
    {_id: ObjectId("62a710c1bd3e6ff14d853d17")},
    {$inc: {"comments.0.vote": 10}}
)

這種做法的一個不好之處是,當文檔有很多,我們無法明確知道是第幾個時,就無法更新了。

定位運算符 $

使用定位運算符可以匹配出查詢文檔所在確切的位置。

db.posts.updateOne(
    {"comments.author": 'name2'},
    {$inc: {"comments.$.vote": 1}}
)

需要注意定位運算符只會更改匹配到的第一個文檔,即使使用我們下面要介紹的 updateMany() 方法也是一樣的效果。對於要更新多個文檔的,我們可以使用下面的數組過濾器。

數組過濾器 arrayFilters

在這裏,我們將投票數大於等於 3 的評論增加一個 hidden 字段。注意 elem 是滿足匹配的一個標識符,名稱可以隨我們定義。

db.posts.updateOne(
    {_id: ObjectId("62a7fd9fbd3e6ff14d853d19")},
    {$set: {"comments.$[elem].hidden": true}},
    {arrayFilters: [
      {"elem.vote": {$gte: 3}},
    ]
)

updateMany()

該方法與 updateOne 不一樣的是,它會更新滿足條件的所有文檔。

findOneAndUpdate()

平時在開發的時候,會遇到這樣一種需求:想要返回修改過的文檔。findOneAndUpdate 方法集成了查找、修改和返回修改過的文檔,並且它還是原子性的,也就是說,多個用戶修改同一個文檔,不會產生併發問題,因爲它內部會進行加鎖,等當前用戶操作完,再到下一個用戶。

Read 查詢文檔

findOne()

對於查詢,我們可以在第一個參數傳入查詢文檔

db.user.findOne({name: 'xiedun'})

find()

當要查詢多個文檔時,可以使用 find() 方法,傳的參數和 findOne() 是一樣的。

一般在查詢的時候,都不會只是匹配某個字段就行了。因此 MongoDB 裏面提供了很多的查詢操作符,可以讓我們執行復雜的查詢。

例如:

// 查詢年齡大於 18 歲的用戶
db.user.find({age: {$gt: 18}})

這裏的 $gt 就是一個查詢操作符。

看下面的這些文檔:

{
    "_id" : ObjectId("62ac1a89bd3e6ff14d853d25"),
    "name" : "name5",
    "status" : "A"
},
{
    "_id" : ObjectId("62ac1a89bd3e6ff14d853d24"),
    "name" : "name4",
    "status" : "B"
},
{
    "_id" : ObjectId("62ac1a89bd3e6ff14d853d23"),
    "name" : "name3",
    "status" : "C"
},
{
    "_id" : ObjectId("62ac1a89bd3e6ff14d853d22"),
    "name" : "name2",
    "status" : "D"
},
{
    "_id" : ObjectId("62ac1a89bd3e6ff14d853d21"),
    "name" : "name1",
    "status" : "E"
}

想要查詢 status 是 A 或者是 B 的文檔,我們可以使用 $in 操作符

db.User.find({status: {$in: ['A', 'B']}})
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d22"),
//     "name" : "name2",
//     "status" : "B"
// },
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d21"),
//     "name" : "name1",
//     "status" : "A"
// }

相反的,如果要查詢 status 不包含 A 或 B,可以使用 $nin 操作符

db.User.find({status: {$nin: ['A', 'B']}})
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d23"),
//     "name" : "name3",
//     "status" : "C"
// },
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d22"),
//     "name" : "name2",
//     "status" : "D"
// },
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d21"),
//     "name" : "name1",
//     "status" : "E"
// }

官方文檔裏總結了所有的查詢操作符,沒必要都背下來,可以對它們有個大致的瞭解,在使用過程中如果忘記了再回來查閱就好了。

這裏要介紹一下什麼是 遊標 ,以及遊標上的一些方法。

使用 find() 方法執行查詢,它會返回一個結果,這個結果就是一個遊標。遊標可以讓我們控制結果的輸出,以及對結果做一些操作,例如限制數量,跳過一些結果和進行排序等操作。

next()  hasNext()  toArray()

得到遊標後,可以調用 next() 方法,它會返回下一個文檔,不過要注意,如果沒有文檔可返回時將會拋出一個錯誤。

var cursor = db.user.find({})
document = cursor.next()
// {
//     "_id" : ObjectId("62ac1a89bd3e6ff14d853d23"),
//     "name" : "name1"
// }

也可以調用 hasNext() 方法判斷是否還有文檔

var cursor = db.user.find({})
cursor.hasNext()  // true or false

一次返回一個結果在某些場景是挺有用的,但我們更多是希望以數組的形式一次返回所有結果,因此可以調用 toArray() 方法。

db.user.find({}).toArray()

不過要注意在 shell 裏面調用和在驅動裏面調用會有些差別,在驅動裏面調用返回的是一個 promise。

const cursor = db.user.find({})
await cursor.hasNext()
await cursor.next()

其實我們平時在項目裏就己經用到了遊標方法,只不過是使用了鏈式調用,沒有分步執行。因爲每個遊標方法都是返回了一個 promise

await Global.collection("test").find().sort({createTime:-1}).toArray()

sort()  limit()  skip() projection()

sort() 方法很常用,例如想要根據創建時間返回查詢結果:

// 1 是正序,-1 是倒序
db.user.find({}).sort({ createTime: 1 })

如果沒有創建時間字段,或者沒有其它字段可以用來排序的。我之前看到有些小夥伴是對返回的結果逆轉一下順序 db.user.find({}).toArray().reverse()。其實可以使用 sort 方法,然後根據 _id 來排序。

db.user.find({}).sort({ _id: -1 }).toArray()

limit() 方法和 skip() 一般是配合來使用在分頁的場景

// 這裏表示跳過前面 20 條數據,查詢後面的 20 條
db.user.find({}).skip(20).limit(20)

.projection() 這個方法可以讓我們決定返回一個文檔的哪些字段,以及哪些字段不返回。利用這個方法,我們還可以做到把 _id 不返回。

// 1 表示返回,0 表示不返回
db.user.find({}).projection({age: 0})

關於遊標需要注意的是,只有 find() 方法纔會返回遊標,像其它方法,例如:findOne(),是不會返回遊標的,因爲它直接返回查詢到的文檔。因此它們也無法使用遊標上的方法。

這裏附上官方文檔總結的所有遊標上的方法:

圖片

總結

MongoDB 涉及到的概念和方法非常的多,我們這次的分享也只是講到了很小的一部分而已。我自己的一個看法是可以先過一遍官方文檔,有個印象,然後在做的過程中逐步加深對某個概念的理解。

鏈接

MongoDB 官方文檔

MongoDB 驅動

TSRPC 框架

查詢操作符

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/I6nUcGCmUsnU8w8IgnuzxQ