跳至主要内容

[Mongo] MongoDB Shell 指令操作(Shell CLI)

# 啟動 MongoDB Shell
$ mongosh

# 查看指令
> help # 檢視和 shell 有關的指令
> db.help() # 檢視和 db 有關的指令
> db.[collection].help() # 檢視和 collections 有關的指令

基本觀念

  • NoSQL: not only SQL

  • 階層關係:

    • database:在 MongoDB server 中可以有多個 database(DB)
      • collection: 用來整理組織 document,類似 SQL 中的 table。一個 db 中可以有多個 collections。
        • document: 個別的紀錄(record),類似 SQL 中的 row。一個 collections 中可以有多個 documents
  • MongoDB 將資料儲存成 BSON 的文件,BSON 是一種以二進位(binary)方式來表徵 JSON 的方法

    • 在操作 MongoDB 時,我們提供的是 JSON,MongoDB 的 drivers 會把這個 JSON 轉成對應的 BSON 在保存在 MongoDB 中
    • BSON 可以包含比 JSON 更多的資料類型
    • BSON 的儲存效率更高
  • MongoDB 的原子性(Atomicity)是建立在 Document Level,也就是說,一個 Document 要嘛有被完整保存成功,要嘛失敗完全沒存,不會有只存了「部分 Document」的情況。

document 是 atomic 的,但整個操作不是

當你使用 insertMany()updateMany() 這類方法一次改動多個 documents 時,是可能會發生部分失敗的情況。但要留意的是,這裡的部分失敗指的是這個「操作」,而不是「document」。如果需要確保整個操作的原子性,需要使用 transaction

基本指令

> help                 # 檢視常用指令
> show dbs # 檢視目前所有的 database
> show collections # 檢視當前 db 有的 collections
> db # 顯示當前所在資料庫
> quit # 離開

資料庫相關

db                  # 顯示當前所在資料庫
show dbs # 顯示現有資料庫(database)
show collections # 顯示當前資料庫內所有的 collections
use [DatabaseName] # 使用此資料庫,如果該資料庫不存在則自動「建立」並使用

##
# 針對 database
##
db.getCollectionNames() # 顯示當前資料庫中的 collection 名稱
db.dropDatabase() # 刪除資料庫

##
# 針對 collection
##
db.myCollection.drop() # 刪除 myCollection 這個 collection
  • 在 MongoDB 中,如果該 database 不存在,MongoDB 會在第一次儲存資料到該 database 時建立該 database

CRUD

CREATE

##
# Create
# db.collection.insertOne({})
# db.collection.insertMany([{}, {}])
##
db.todos.insertOne({ title: 'Hello Mongo' }); # 建立 document

Ordered Behavior

預設的情況下,MongoDB 使用的是 ordered behavior,在新增資料時如果發生錯誤(例如,duplicate key error):

  1. 它不會有 rollback 的行為,成功寫進去的資料不會因為其中一個資料寫不進去而被還原
  2. 預設當 orderedtrue 時,一旦發生錯誤,在錯誤發生後的資料就不會繼續寫入。如果希望它能夠忽略掉這個錯誤,繼續寫入後面的資料,則可以把 orderer 設成 false
db.hobbies.insertMany(
[
{
_id: 'hiking',
name: 'hiking',
},

{
_id: 'yoga',
name: 'yoga',
},
{
_id: 'cooking',
name: 'cooking',
},
],
{ ordered: false },
);

Write Concern

官方文件

Write Concern 的目的是確保 MongoDB 對寫操作的確認程度(acknowledgement level)。它允許用戶指定在寫操作被認為成功之前,MongoDB 必須接收到的確認數量。這可以確保數據的耐久性(durability)和一致性(availability),例如在單一伺服器、複寫集(replica sets)或分片集群(shared clusters)中,提供不同程度的數據安全保障。較高的 Write Concern 能增加數據安全性,但可能會降低效能。

如同這張圖所示:

image-20240623213023345

MongoDB 在把資料寫進資料庫時,為了效能,實際上還有一個 Memory 存在;另外,需要的話,還可以寫一個實體的 Journal 檔,如此,一旦資料庫發生錯誤,只要有寫了 Journal 檔,還是可以把沒有寫入到資料庫中的資料復原。

透過 Write Concern 就可以設定這個行為:

{ w: <value>, j: <boolean>, wtimeout: <number> }
  • w
    • Durability:預設的情況下是 w: majority,表示大部分的 replica set 需要確認完成寫的東西後,才算成功。當資料是不可遺失的話,可以設定更高的 write concerns。
    • Performance:如果你的使用情境非常重視寫入的效能,並且能夠容忍可能的資料流失,則可以把 write concern 設成 w: 1w: 0
  • j:用於指定寫操作在資料寫入日誌(journal)後才會得到確認(acknowledge)。日誌是一種預寫日誌(write-ahead log),用來確保在錯誤發生時數據的完整性
    • jtrue 時,表示 MongoDB 確保寫操作只有在數據成功寫入日誌(on-disk journal)後才會被確認,但相對的會犧牲效能
  • wtimeout:單位是 ms,目的是避免 w 設定的節點無限期等待
Acknowledgment Behavior

當使用情境不同的,例如在 Standalone 或 Replica Sets 時,wj 會有不同的行為,詳細可以參考官方文件的說明。

不等待資料是否寫進 memory 中:

db.hobbies.insertOne('writing', { writeConcern: { w: 1, j: true, wtimeout: 200 } });
// {
// acknowledged: false,
// insertedId: ObjectId('66782454b1ed5d2a0fbe0dce')
// }

READ

keywords: find(), findOne(), count(), distinct()
##
# Read
# db.collection.find()
# db.collection.findOne()
# db.collection.countDocuments()
# db.collection.estimatedDocumentCount()
# db.collection.distinct()
##
db.todos.countDocuments(); # todos 中有多少 documents
db.todos.estimatedDocumentCount();

db.todos.find(); # 檢視某一 Collections 內所有的 documents
db.todos.find({ title: 'Create new Node course' }); # 檢視特定 document
db.todos.find({ _id: ObjectId('5c0beffdeb866557d13c0ef8') }); # 檢視特定 document

db.todos.find().pretty(); # 以格式化的方式列出 document
db.todos.find().limit(2); # 指列出前兩筆 document
db.todos.find().skip(2); # 忽略前兩筆資料
db.todos.find().sort({ title: 1 }); # 排序資料,1 是 ascending,-1 是 descending
db.todos.find().toArray()
db.todos.find().forEach()

Projection:SELECT fields

// Projection(類似 SQL 的 SELECT,只選出有需要用到的欄位,確保效能)

db.todos.find({}, { title: true }); // 會顯示 _id 和 title
db.todos.find({}, { _id: false, title: true }); // 只會顯示 title
db.todos.find({}, { title: false }); // 除了 title 其他都會顯示
db.todos.find({}, { title: true, tag: false }); // 錯誤的語法

Query Elements in an Array

// Query 尋找陣列中的資料
// 假設 hobbies 是 array,我們想要找 hobbies 中包含 'sports' 的 document
// hobbies: ['sports', 'video game']
db.todos.find({ hobbies: 'sports' });
warning

上面的 query 會找到在陣列中「包含」有該元素的資料,如果你想要找到是在該陣列中「只有那個元素」,則要使用 hobbies: ["sports"]

Query Embedded Documents (Nested Object)

可以在 key 的地方使用 "x.y.z",就可以搜尋 embedded documents(nested objects)

db.todos.find({ 'status.description': 'on-time' });
db.movies.find({ 'rating.average': { $gte: 6.7 } });

Query Array of Object

如果是 Array of Object,例如:

#    history: [
# { disease: 'headache', treatment: 'aspirin' },
# { disease: 'fever', treatment: 'ibuprofen' }
# ]

如果我們希望找到 history 中有 disease 是 'headache' 的元素,可以使用:

db.patientData.find({ 'history.disease': 'headache' });

當如果你希望找到的是 history 中有完全相同的物件,則是使用:

# 可以用類似的方式來 query
db.patientData.find({ history: { disease: 'headache', treatment: 'aspirin' } });

Operator

Comparison Operators
// $eq: equal(預設沒帶 operator 的話就是這個)
// $gt: greater than
// $gte: greater than or equal to
// $lt: less than
// $lte: less than or equal to
// $ne: not equal
db.todos.find({ priority: { $gt: 2 } });
db.movies.find({ runtime: { $ne: 60 } });
db.movies.find({ runtime: { $in: [] } });

// $in 和 $nin operator
db.todos.find({ status: { $in: ['A', 'D'] } }); // status 的值是「'A' 或 'D'」都符合
db.movies.find({ runtime: { $in: [30, 42] } }); // runtime 的值是「30 或 42」都符合
db.movies.find({ runtime: { $nin: [30, 42] } }); // runtime 的值不是「30 或 42」都符合
Logical Operators
// $or operator
db.todos.find({
$or: [{ title: 'Create new Node course' }, { title: 'Learn NodeJS' }],
});

db.movies.find({
$or: [{ 'rating.average': { $lt: 5 } }, { 'rating.average': { $gt: 9.3 } }],
});

// $nor operator:表示不是 xxx 也不是 xxx
db.movies.find({
$nor: [{ 'rating.average': { $lt: 5 } }, { 'rating.average': { $gt: 9.3 } }],
});

// $and operator,預設不指定就是 $and
db.movies.find({
$and: [{ 'rating.average': { $gt: 9 } }, { genres: 'Drama' }],
});
// 等同於
db.movies.find({ 'rating.average': { $gt: 9 }, genres: 'Drama' });

之所以會需要 $and operator,是因為在 JavaScript 中,物件中不能有重複的 key。舉例來說,如果我們想要找出 genres 這個 array 中同時有 HorrorDrama 這兩個 elements 的:

// ❌ JavaScript 並不能這樣寫,因為當物件中有相同的 key,後面的會改掉前面的
db.movies.find({ genres: 'Horror', genres: 'Drama' });

// 所以上面的寫法實際上等同於
db.movies.find({ genres: 'Drama' });

// 如果要達到原本的目的,就要用 $and operator
db.movies.find({ $and: [{ genres: 'Horror' }, { genres: 'Drama' }] });

但如果是在 Golang 的 mongo driver 中,不用 $and 即可可以達到原本的目的:

filter := bson.D{{"genres", "Drama"}, {"genres", "Horror"}}

UPDATE

##
# Update
# db.collection.updateOne({}, {})
# db.collection.updateMany({}, {})
# db.collection.replaceOne()
# 第一個參數的物件稱作 update filter,類似 find
# 第二個參數的物件稱作 update action,透過 $set 可以更新內容
##
db.todos.updateOne(
{ _id: ObjectId('5c0beffdeb866557d13c0ef8') },
{ $set: { tag: 'course', title: 'Learn Node JS' } }
);

DELETE

##
# Destroy
# db.collection.deleteOne()
# db.collection.deleteMany()
##
db.todos.deleteOne({ tags: 'course' })
db.todos.deleteMany({ tags: 'course' }) # 刪除 tags 為 'course' 的 documents
db.todos.deleteMany({}) # 刪除 todos 中所有的 documents
  • db 指我們所在的資料庫
  • postcollection ,會在 insert document 之後被建立
  • {title: 'Hello Mongo'} 是所見建立的 document
  • 過去使用的 insert(), update() 寫法目前處於 deprecated 狀態,建議不要使用。

建立有資料型別驗證(Document Validation)的 Collection

bsonType

可用的 bsonType 可以參考這裡的官方文件。

schema validation

關於 schema validation 可以這裡的官方文件。

預設的情況下,MongoDB 不會對 Document 做任何資料型別的驗證,但如果有需要,可以在建立 Collection 的時候,利用 createCollection 就把 Collection 中 Document 中的欄位名稱和資料型別定義好:

db.createCollection('users', {
// 定義 users 這個 collection 的 validation rules
validator: {
$jsonSchema: {
bsonType: 'object',
// 這個 object 中有哪些欄位是必填的
required: ['email', 'age', 'address', 'habits'],

// 定義每個欄位的型別
properties: {
email: {
bsonType: 'string',
description: 'must be a string and is required',
},
age: {
bsonType: 'int',
description: 'must be a integer and is required',
},
// embedded / nested document
address: {
bsonType: 'object',
required: ['country', 'addressLine'],
properties: {
country: {
bsonType: 'objectId',
description: 'must be objectId and is required',
},
addressLine: {
bsonType: 'string',
description: 'must be string and is required',
},
},
},
// array
habits: {
bsonType: 'array',
description: 'must be an array and is required',
// 定義 array 裡的 items 的型別
items: {
bsonType: 'string',
description: 'must be a string and is required',
},
},
},
},
},
});

當有不符合 validation rules 的 document 要存進 mongoDB 時,就會噴錯。

其他指令

匯入資料(載入 seed 檔)

官方文件

如果想要把某個 JSON 檔匯入到 Database 的某個 Collection 中,可以使用 mongoimport

# --jsonArray,如果 import 的檔案中包含多個 documents,則要加上這個 option
# --drop,如果 collection 已經存在,先移除舊、建立新的後再匯入
$ mongoimport example.json -d [databaseName] -c [collectionName] --jsonArray --drop

索引相關

db.todos.getIndexes(); // 取得 todos 這個 document 的索引
db.todos.createIndex({ title: 1 }); // 建立索引,1 表示越來越大,2 表示越來越小
db.todos.dropIndex('title_1'); // 刪除索引

Relations

在 MongoDB 中,可以使用 embedded / nested documents 或 reference 的方式來將不同的 collections 建立關聯,至於要選擇哪個方式,可以思考的角度包括:

需求embedded / nested documentsreferences
資料分析X(使用 embedded 的話,後續不好將所有 embedded 的 documents 拉出來做資料分析)O(比較好做後續的資料分析,例如,想要看某一個產品的銷量)
經常會需要同時用到的資料O(使用 embedded 的話,關聯的資料會直接拉出,不需要再另外 query,但相對來說,資料的傳輸量會比較大)X(需要另外 query,但如果不需要另一個表的資料,則可以節省流量)
需要同時更新參照到的資料X(每個 embedded 中的 documents 都是獨立的,沒辦法一次全部更新)O(因為使用 reference,所以改 reference 的資料來源就可以一併更改所有使用到的地方)
需要建立 snapshotO(和上一列的情況相反,如果我希望儲存的資料後續不會因為 reference 的內容改動而被修改,而是希望保留一開始寫入的值,例如商品快照)X
避免資料重複X(因為 embedded 中的資料都是彼此獨立的,所以容易出現重複的資料,例如不同 Order 中的 Product 都是一樣的,就會重複同樣的內容好幾次)O(減少重複的資料,而是全部都 reference 到同一個資料來源)
資料量很大X(因為 documents 本身有 16MB 的限制,且最多只能 100 層,所以如果資料量很大時,全部塞在同一個 nested document 中並不是很好的做法)O(只存 reference,所以不太會碰到 document 的容量上限)

One To One Relations

在 MongoDB 中,如果是一對一的關係,通常可以用 Embedded/Nested Documents 的方式完成就好。

舉例來說,每個 User 都會有一個 Address,可以直接把 Address 以 Embedded Documents 的方式保存起來:

db.users.insertOne({
name: 'Aaron',
age: 35,
address: { country: 'Taiwan', city: 'Taipei' },
});

但有些時候可能有資料分析的需求,例如,希望知道居住在不同城市的會員人數,這時候就可以用類似關聯式資料庫的方式,把 user_id 保存在 Address 這個 collection 中(Address belongs to User):

db.users.insertOne({ name: 'Aaron', age: 35 });
// {
// acknowledged: true,
// insertedId: ObjectId('666dc2e480de2f9394703a21')
// }

db.addresses.insertOne({
country: 'Taiwan',
city: 'Taipei',
user_id: ObjectId('666dc2e480de2f9394703a21'),
});

當要 Query 某個 User 的 Address 時,就可以使用:

const userId = db.users.findOne({ name: 'Aaron' })._id;
db.addresses.findOne({ user_id: userId });

One to Many Relationship

和 One to One Relationship 類似,如果要做 One to Many Relationships 的話,一樣可以使用 embedded documents 或 reference 的方式,選擇那種端看應用程式的需求。

舉例來說,如果是類似 Facebook 中一個 Post 底下會有很多不同 comments 的情況,如果我們希望每次拿到 Post 的時候,連帶也把底下的 comments 一併拿出來,同時,如果我們有沒有單獨需要分析 comments 的需求的話,這時候就可以使用 embedded documents。

然而,如果你不希望在拿 Post 的時候,就一併把所有的 comments 也同時取出,而是當這則貼文被點擊時才去 fetch 對應的 comments;又或者,你發現使用 embedded documents 有可能有機會超過 document 最多 16MB 的限制,這時候則應該考慮使用 reference 的做法,也就是只存用來 reference 用的 ObjectId。

Many to Many Relationship

在 MongoDB 中 Many to Many 的關聯一樣可以透過 embedded documents 或 reference 來達到。

如果資料是需要連動修改的,這種就會適合用 reference,例如 Author 和 Book 的關係。一個 Author 可以有出版很多書,一本書也可以同時擁有多個作者。一般來說,作者的資訊一旦更新,我們會希望更新到所有有參照到這個作者的地方,這種就適合用 reference。

另一個例子是一個 Orders 中有很多 Products,一個 Products 也可以屬於很多 Orders。如果使用 Embedded 的話,即使 Product 更新後,Order 裡的 Product 並不會被更新到,它比較像是建立了一個商品快照(snapshot)的概念,所以如果 Product 名稱有修改,Order 中的 Product 名稱並不會被更新。

除此之外,使用 Embedded 較容易出現重複的資料,即使不同 Order 中,買的是相同的產品,還是會重複複製好幾份一樣的資料,例如:

[
{
_id: ObjectId('6671a57466cca4d69fd8627e'),
orderNumber: '111',
products: [
{
title: 'Learn MongoDB',
price: 100,
quantity: 3,
},
],
},
{
_id: ObjectId('6671a54666cca4d69fd8627d'),
orderNumber: '222',
products: [
{
title: 'Learn MongoDB',
price: 100,
quantity: 1,
},
],
},
];

但如果使用 reference 則會是:

[
{
_id: ObjectId('6671a57466cca4d69fd8627e'),
orderNumber: '111',
products: [
{
productId: ObjectId('6671a5b266cca4d69fd8627f'),
quantity: 3,
},
],
},
{
_id: ObjectId('6671a54666cca4d69fd8627d'),
orderNumber: '222',
products: [
{
productId: ObjectId('6671a5b266cca4d69fd8627f'),
quantity: 1,
},
],
},
];

參考