# Install MongoDB on Docker
MongoDB是一款開源的NoSQL資料庫,採用BSON格式(類JSON)儲存資料,官方在Docker Hub有提供印象檔方便開發者直接取用。本篇適合具有Docker基礎知識的人閱讀。
## 透過Docker來建立
要在Docker的環境下使用MongoDB,最簡單的方式便是直接執行Docker run指令,但在執行之前,應該要先建立Docker Volumn來保存資料。
Step 1. 建立Volume
docker volume create --name mongoVolume
>可以使用docker volume ls來確認
Step 2. 執行MongoDB
docker run -d --name mongo -v mongoVolume:/data/db -p 27017:27017 mongo
>指令說明:
-v:掛載剛才建立的mongoVolume。
-p:建立對外的連線埠號為27017,這是mongodb預設的網路埠號。
-d:背景執行。
Setp 3. 使用MongoDB client來測試是否可以順利運行
C:\>docker exec -it mongo bash
root@bcca2e167696:/# mongo
------------OR-------------
C:\>docker exec -it mongo mongo
>上述二種方式都可以進入Docker內部執行mongo指令,在本次的範例中都可以取得以下的畫面:

另外也可以透過Studio 3T、NoSQLBooster等軟體(類似SQL Manager)或是啟用另一個MongoDB client來測試連線是否正常。
C:\>docker run -it --link mongo:mongo --rm mongo sh -c "exec mongo "$MONGO_PORT_27017_TCP_ADDR:$MONGO_PORT_27017_TCP_PORT/test""
Step 4. 嘗試寫入資料
MongoDB是使用指令來操作資料,client啟動後,使用下列指令來新增資料
```shell=
※啟用mongoDB
use school
db.student.insert({"name": "ken"})
db.student.find()
※返回JSON格式的結果
{ "_id" : ObjectId("5d9ff305dba4848a705969bf"), "name" : "ken" }
```
>*use school*可用來告訴MongoDB切換到一個叫做school的資料庫,在執行*insert()*之前若資料庫不存在,mongoDB會在*insert()*時自動建立,最後再用*find()*來查詢完整資料。
## 建立權限
MongoDB預設是不需要帳密的,因此剛安裝完成時,任何人都可以存取資料庫。由於MongoDB必需先建立管理者才會啟動驗證機制,因此當務之急便是建立一個資料庫的管理者帳號,帳號建立後便可以為每一個資料庫建立使用者帳號來隔離資料庫的存取權,提升安全性。
Step 1.建立管理者帳號
use admin
db.createUser(
{
user: "dbadmin",
pwd: "password",
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})
> role 只接受`userAdmin` 與 `userAdminAnyDatabase`
Step 2. 以認證模式啟動
一般情況下,需要修改MongoDB的設定檔並將MongoDB重啟即可,但在Docker的環境下,只需要將容器關閉並移除,再重新建立時掛上相同的volumn並且要加上`--auth`參數來開啟認證模式。
docker stop mongo
docker rm mongo
docker run -d --name mongo -v mongoVolume:/data/db -p 27017:27017 mongo --auth
重新連線後會發現看不到任何資料庫,這是因為啟用了認證模式,須改用管理者帳密登入MongoDB。一般情況下可以使用mongo client 搭配參數執行連線。
mongo --host <ip> --port 27017 -u "dbadmin" -p "password" --authenticationDatabase "admin"
或者先以無帳號的方式登入後再使用帳號與密碼
mongo --host localhost --port 27017
--------------登入後----------------
use admin
db.auth("dbadmin","password")
Step 3.加入一般的資料庫使用者
使用管理者權限登入後才有權限新增用戶,新增的方式是切換至目標資料庫後建立新的使用者。mongoDB的資料庫權限是根據使用者決定的。
use testdb
db.createUser(
{
user: "user",
pwd: "userpassword",
roles: [ { role: "readWrite", db: "testdb" } ]
}
)
Step 4.改以一般用戶來連線
接下來就可以透過一般用戶的帳號來建立連線,登入後將只能看到被授與權限的資料庫。
mongo <ip>:27017/cool_db -u "user" -p "userpassword"
上述的步驟完成了MongoDB的基本安全與權限設定,但在實務上應該學習使用Studio 3T這類工具來操作資料庫,接下來會說明如何利用C#與MongoDB溝通。
使用C#進行連線
---
一般而言,撰寫程式來操作資料庫大多是基於業務需求,因此需要學習資料庫的連線方式以及新刪修查的語法。以C#為例,官方提供了`MongoDB.Driver`套件,在語法上與mongoDB自帶的shellscript的語法非常相似。

首先,開啟Nuget或透過指令安裝套件。
Install-Package MongoDB.Driver
順利安裝完成後,就跟所有的資料庫操作一樣,第一步是要建立連線,MongoDB提供了許多建立連線的語法,其中最簡單的方式就是直接引入連線字串。
```csharp=
MongoClient client = new MongoClient("mongodb://user:userpassword@127.0.0.1:27017/testdb");
```
若是較完整的應用情境,例如想取得MongoDB在執行時期的語法,可以參考以下的方式。
```csharp=
var client = new MongoClient(new MongoClientSettings
{
Server = new MongoServerAddress("localhost", 27017),
Credential = MongoCredential.CreateCredential(databaseName: "testdb", username: "user", password: "userpassword"),
ClusterConfigurator = cb =>
{
cb.Subscribe<CommandStartedEvent>(e =>
{
Console.WriteLine($"{e.CommandName} : {e.Command}");
});
}
});
```
若是成功建立MongoClient物件,那就代表連線正常,接下來便是取得資料庫與資料表了。
建立資料表
---
在說明程式碼的部份之前,首先應該要認識MongoDB的資料結構。MongoDB的也有類似關聯式資料庫的結構存在,下表便是二者之間的異同:
| 關聯式資料庫 | MongoDB | 備註 |
| -------- | -------- | --------------- |
| database | database | |
| table | collection | 不支援join |
| row | document | 自動以`_id`作為key|
| column | field | |
| index | index | |
`collection`屬於Key/Value的結構,因此無法跨資料表進行操作,而`document`才是實際的資料物件,在結構上為[BSON](https://zh.wikipedia.org/wiki/BSON)。預設每一個`document`都會有一個名為`_id`的`field`來儲存一個有序的16進制作為`collection`的鍵值。

>從上圖可以清楚的知道`document`有四個field,而除了`_id`之外的欄位都是可以為空值。
在對於`document`有了基本認知之後,便可以嚐試使用MongoClient來取得database與collection
```csharp=
IMongoDatabase db = client.GetDatabase("testdb");
IMongoCollection<User> collection = db.GetCollection<User>("user");
```
在首次執行時,即便特定的Database不存在,在成功使用`GetDatabase`方法後,預設會自動建立指定名稱的Database;而`GetCollection<T>`也是則會依照泛型物件來建立*collection*以及*collection*的名稱
新刪修查
---
由於網路上有許多教學資源,所以觀念與基本操作的部份就不多說,可以參考[30天之你好MongoDB](https://ithelp.ithome.com.tw/users/20089358/ironman/1064),先有一個基本的認知之後再開始學習開發套件。
### 新增
依循著鐵人賽的軌跡,首先是學習如何新增資料;MongoDB在C#中提供的insert方式有`InsertOne`、`InsertMany`二種;另外還有一種批次(Bulk)作業,與一般的作業差別在於批次作業在執行時不會被立刻寫入而資料庫中,而是會等到適合的時機才進行寫入的動作,此特性非常適合應用在大量且非同步的情境。
底下的範例使用了自定的物件`User`,比較三種insert方式的效能
```csharp=
internal class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Sex { get; set; }
}
--------------測試程式----------------
IMongoDatabase db = client.GetDatabase("testdb");
IMongoCollection<User> collection = db.GetCollection<User>("user");
var user = new User()
{
Name = "DDD",
Age = 2,
Sex = "W"
};
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();//引用stopwatch物件
sw.Start();//碼表開始計時
int count = 100;
for (int i = 0; i < count; i++)
{
collection.InsertOne(user);
}
sw.Stop();//碼錶停止
Console.WriteLine($"When use InsertOne 100 times to insert 100 user object then execute time is {sw.Elapsed.TotalMilliseconds.ToString()}");
sw.Reset();//碼表歸零
List<User> users = new List<User>();
for (int i = 0; i < count; i++)
{
users.Add(new User()
{
Name = "DDD",
Age = 2,
Sex = "W"
});
}
sw.Start();
collection.InsertMany(users);
sw.Stop();
Console.WriteLine($"When use InsertMany 1 times to insert 100 user object then execute time is {sw.Elapsed.TotalMilliseconds.ToString()}");
sw.Reset();
List<WriteModel<User>> models = new List<WriteModel<User>>();
for (int i = 0; i < count; i++)
{
models.Add(new InsertOneModel<User>(new User()
{
Name = "DDD",
Age = 2,
Sex = "W"
}));
}
sw.Start();
collection.BulkWrite(models);
sw.Stop();
Console.WriteLine($"When use BulkWrite 1 times to insert 100 user object then execute time is {sw.Elapsed.TotalMilliseconds.ToString()}");
sw.Reset();
```
此範例用是以同步的方式來執行,觀察寫入1筆資料與寫入100筆資料的執行效能,可以發現一件有很趣的情形,MongoDB在寫入單筆資料的效能奇差,無法理解為何會有如此大的差異(InsertOne存在的原因是…?)。
| 資料量 | InsertOne | InsertMany | BulkWrite |
| ----- | --------- | ---------- | --------- |
| 1 | 353.37ms | 4.11ms | 5.47ms |
| 100 | 550ms | 19ms | 17ms |
| 100000| 86891.70ms| 719.35ms | 734.64ms |
### 刪除
在C#為了刪除MongoDB的資料資料,提供了`DeleteOne`、`DeleteMany`以及`Bulk`的方法,`DeleteOne`是只刪除第一筆符合條件的資料,而`DeleteMany`則是刪除所有符合條件的資料。在C#,使用`FilterDefinition<TDocument>`類別來實作query區段的內容,而`FilterDefinition<TDocument>`可以透過靜態類別Builders的Filter屬性(型別FilterDefinitionBuilder)來取得。
```csharp=
collection.DeleteMany(Builders<User>.Filter.Eq(a => a.Name, "DDD"));
```
>等同於SQL的DELETE FROM User WHERE Name = 'DDD';
### 修改
MongoDB同樣為了更新增料提供了`UpdateOne`、`UpdateMany`以及`Bulk`。`UpdateOne`與`UpdateMany`的基本參數一致,差別在於UpdateOne則是只更新符合條件的第一筆`document`。在使用上可以透過一個靜態Builders類別所建立的`FilterDefinition<TDocument>`以及`UpdateDefinition<TDocument>`類別來實作更新的條件與更新的內容。
```csharp=
collection.UpdateMany(Builders<User>.Filter.Eq(a => a.Name, "DDD"),
Builders<User>.Update.Set(a => a.Name, "Yowko"),
new UpdateOptions() { IsUpsert = false});
```
>等同於SQL的UPDATE User SET Name = 'Yowko' WHERE Name = 'DDD'
其中比較需要注意的是UpdateOptions類別的IsUpsert屬性,若是為True且更新條件不存在時則自動新增,在使用上需特別注意。在範例中的`Set`使用的是修改器`$set` ,是要讓Age屬性加1,可以改用`Inc`,對應到的修改器為`$inc`。
若是想要以批次的方式來作業,可以使用`UpdateOneModel`或`UpdateManyModel`來實現。
```csharp=
List<WriteModel<User>> models = new List<WriteModel<User>>()
{
new UpdateManyModel<User>(Builders<User>.Filter.Eq(a => a.Name, "DDD"),
Builders<User>.Update.Set(a => a.Name, "Yowko"))
};
collection.BulkWrite(models, new BulkWriteOptions { IsOrdered = false });
```
### 查詢
MongoDB提供了find方法來查詢資料,實作查詢條件除了上述的`FilterDefinition<TDocument>`之外,還可以使用LINQ的`Expression<Func<TDocument, bool>>`語法來實作。
```csharp=
var user = collection.Find(Builders<User>.Filter.Eq(a => a.Name, "DDD")).FirstOrDefault();
----------------OR-------------------------
var user = collection.Find(a => a.Name == "DDD").FirstOrDefault();
----------------OR-------------------------
var user = collection.Find(new BsonDocument(nameof(User.Name), "DDD")).FirstOrDefault();
```
如果希望完整查詢,可以使用BsonDocument或是模擬的LINQ語法
```csharp=
List<User> userlist = collection.Find(new BsonDocument()).ToList() ;
----------------OR-------------------------
List<User> userlist = collection.Find(a => true).ToList();
----------------OR-------------------------
List<User> userlist = collection.Find(Builders<User>.Filter.Empty).ToList();
```
若資料量非常龐大,也可以考慮使用疊代的方式來作業
```csharp=
await collection.Find(a => true).ForEachAsync(u => Console.WriteLined(u));
----------------OR-------------------------
foreach(var u in collection.Find(a => true).ToEnumerable())
{
Console.WriteLined(u);
}
```
若希望搜尋後進行排序
### 批次處理
批次處理可以分為**有序**與**無序**二種方式;在執行的過程中若拋出異常,有序的批次處理便會中斷作業,無序的批次處理則反之。
```csharp=
List<WriteModel<User>> models = new List<WriteModel<User>>()
{
new InsertOneModel<User>(new User()
{
Name = "DDD",
Age = 2,
Sex = "W"
}),
new InsertOneModel<User>(new User()
{
Name = "DDD",
Age = 2,
Sex = "W"
}),
new UpdateManyModel<User>(Builders<User>.Filter.Eq(a => a.Name, "DDD"),
Builders<User>.Update.Set(a => a.Name, "Yowko"))
};
collection.BulkWrite(models, new BulkWriteOptions { IsOrdered = true });
```
>若IsOrdered屬性設為True,則採用有序的方式;預設為false。
# 附錄
MongoDB有為`FilterDefinition<TDocument>`實作自訂運算元,讓開發者在過濾資料時可以合併多個`FilterDefinition<TDocument>`物件。

```csharp=
var filterBuilder = Builders<BsonDocument>.Filter;
var filter = filterBuilder.Gt("Age", 0) & filterBuilder.Lte("Age", 5);
var user = collection.Find(filter).FirstOrDefault();
```
>可以得到0 < Age <5的User物件。
參考
---
- [如何在 Windows 環境安裝及設定 MongoDB](https://blog.yowko.com/windows-mongodb/)
- [MongoDB 常用命令介紹](https://blog.csdn.net/u013066244/article/details/53874216)
- [MongoDB 官方技術文件](https://docs.mongodb.com/manual/)
- [YAML metadata](/s/yaml-metadata)
- [Features](/s/features)
Themes
---
- [Dark theme](/theme-dark?both)
- [Vertical alignment](/theme-vertical-writing?both)
###### tags: `MongoDB` `Docker`