---
title: MongoDB
tags: 網頁後端技術文章
---
# MongoDB
[TOC]
---
## 基本觀念
在mongoDB中Table視為Collection,Data為Document,通常以Json格式,當Json資料裡面又有Json資料則稱為嵌套。
:::warning
單個BSON最大16M,
:::
schema == struct
### MongoDB資料類別
* String
* Boolean
* Number(32bits)
* ObjectID
* Date
* Array
### 關係(Relationship)

一對一關係不利於分析
多對多關係

## 環境創建
1. 到[mongo官方網站](https://www.mongodb.com/3)安裝Server(Community)。
2. 下載完成後將目錄複製到根目錄底下
3. 在跟目錄下創建一個data的資料夾
4. 複製data的絕對路徑,並移動至bin目錄。

透過--dbpath設定路徑後就會自動執行。
```bash=
./mongod --dbpath "/Users/chenshiyu/mongodb/data"
```
開啟一個新的命令列,並移動到bin底下輸入`./mongo`即可開啟mongo的shell。

:::warning
一定要在Server運行時才可以啟用
:::
### 設定後台執行
首先在mongo的資料夾中建立一個log的資料夾,透過命令行輸入以下指令設定log的輸出。
```bash=
mongod --dbpath "/Users/chenshiyu/mongodb/data" --logpath "//Users/chenshiyu/mongodb/log/mongo.log" --fork
```
當設定好log的輸出後,我們會發現其實mongo在後台執行。
## 基本指令
我們可以透過以下指令來查詢當前運行的mongo
```bash=
ps -ef | grep mongod
```
也可以透過kill指令來刪除程式
```bash=
kill 運行編號
```
一些關於mongoDB的基礎指令
```bash=
# 清理
cls
# 離開
exit
# 查看當前資料庫
show dbs
# 使用資料庫,若未創建則會自動建立,但是必須建立Collection才會顯示
use 資料庫名稱
# 查看資料庫所有命令
db.help()
# 創建Collection
db.createCollection("名稱")
# 返回當前使用的資料庫名稱
db.getName()
# 返回所有Collection
db.getCollectionNames()
# 刪除資料庫
db.dropDatabase()
```
我們可以設定一筆資料給尚未創建的Collection,當我們賦予資料時Collection會即時被創建,如下我們透過`use demo`使用demo資料庫,並且在該資料庫新增一筆資料,該資料檔案格式為Json,且創建在一個名為employee的Collection,此時employee尚未被創建,當我們新增資料後會自動創建。
```bash=
db.employee.insertOne(json資料)
```
我們可以在Json資料那個欄位中帶入以下資料
```json=
{
"first_name": "Robin",
"last_name": "Jackman",
"title": "Software Engineer",
"salary": 3000,
"Internship": true
}
```

透過以下指令可以進行Collection的查詢
```bash=
# 查詢指令
db.employee.find()
# 美化查詢結果
db.employee.find().pretty()
# 指定ID的添加資料
db.employee.insertOne({"a":1, "b":2, "_id":想指定的id不可重複})
# 刪除指定資料
db.employee.deleteOne(filter)
# 刪除id=1的
db.employee.deleteOne({"_id":1})
# 刪除a=1的資料(a=1),若資料有多筆則優先刪除先進者。
db.employee.deleteOne({"a":1})
# 刪除多筆a=1的資料(a=1)
db.employee.deleteMany({"a":1})
# mongo中的比較符號$gt,當我們要刪除salary>2000的資料時
db.employee.deleteMany({"salary":{"$gt":2000}})
# 插入多個資料
db.employee.insertMany([Json資料])
# 查詢多筆資料透過filter,indOne內建pretty所以不需要特別加
db.employee.find({"title" : "Software Architect"}).pretty()
# 透過updateOne更新資料,需要增加$set修飾字
db.employee.updateOne(filter,{$set:{要修改的內容}})
# 更新資料為salary:200
db.employee.updateOne({"_id" : ObjectId("6033cb0212576a5889dd3dae")},{$set:{"salary":300}})
# 如果單純使用update則如同PATCH,一次更新多個,且不需$set
db.employee.update({"_id" : ObjectId("6033cb0212576a5889dd3dae")},{"salary":300})
# 如果使用repleaceOne則相當於更新一個的update
db.employee.replaceOne(filter,data)
# find 最多只會顯示20筆資訊,輸入it可以顯示更多
db.employee.find()
# 返回特定資訊(節省效能),第一個filter為空代表返回全部,屬性設1代表返回內容。
db.employee.find({}, {first_name:1, last_name:1, salary:1})
# 透過一個filter查詢Json中陣列內含的值
db.employee.find({hobby:"book"}).pretty()
# 透過多個filter查詢Json中陣列內含的值,利用中括弧。
db.employee.find({hobby:["book","movie"]}).pretty()
# 透過$in 查詢包含其中一個參數,此範例查詢包含book或movie
db.employee.find({hobby:{$in:["book","movie"]}}).pretty()
# 查詢內含嵌套的資訊
db.employee.findOne({"嵌套的欄位":值})
db.employee.findOne({"contact.phone":1111})
# 懶人請複製
db.myFristDB.insertMany([{name:"A", age:10},{name:"B",age:12},{name:"C",age:13}])
```
針對特定對象查詢特定欄位。
```bash=
# filter直接查詢所有,再透過查詢後的結果輸出title值
db.employee.findOne({}).title
# 如果對象有內嵌資料則可以透過多個.來訪問
db.employee.findOne({}).contact.phone
```
透過typeof查詢欄位型態
```bash=
typeof db.employee.findOne({}).contact.phone
```
時間的兩種類型
```bash=
# ISODate
var a = new Date()
# Timestamp (1970)
var b = new Timestamp()
```
數字型態
```bash=
# 宣告長整數
var a = NumberLong(1233)
```
透過程式語言操作mongoDB須參考官方Driver。
不推薦使用Insert語法,因為不會回傳ID回來。
若一次插入大量資料到db裡面時,此時若有相同的id,前面的插入不會失敗,但是在該相同id的資料後插入的資料會失敗。

透過find來檢視

透過ordered來改變預設模式,我們可以將ordered設為false讓他走訪每一個。
```bash=
db.test.insertMany([{_id:2, b:1}, {_id:1, c:1}, {_id:3, d:4}],{ordered:false})
```
再進行一次查詢後發現,成功添加。

## 常用操作
### mongoImport
在使用mongoImport之前,需要先到[官網下載](https://www.mongodb.com/try/download/database-tools)mongo database tools,並將bin資料夾的所有內容複製到mongodb的bin裡面。

如果我們要引入Json檔案時,可以透過mongoimport將檔案的絕對路徑進行複製,透過`-d`指定資料庫,`-c`指定collection(table),`--jsonArray`則代表該Json是有多筆資料並以陣列形式存放,`--drop`則表示若該collection已存在則刪除該collection。
```bash=
mongoimport /Users/chenshiyu/Downloads/mongoImport.json -d test -c demo --jsonArray --drop
```
### 簡介method、filter、operator

透過$and 尋找多條件
```bash=
db.movie.find({$and:[{條件1:值},{條件2:值}]})
# 搜尋評分>7 且 評分<8
db.movie.find({$and:[{imdb_score:{$gt:7}},{imdb_score:{$lt:8}}]}).count()
```
nor 就是 or 的相反
$exits = True 代表存在
查詢單個內嵌資料的語法
```bash=
db.people.find({"欄位.內部欄位":"值"})
db.people.find({"hobby.type":"Movie"})
```
查詢內部欄位資料數量的語法
```bash=
# 透過$size修飾字來查詢hobby數量為2的資料
db.people.find({hobby:{$size:2}})
```
透過多條件查詢內部欄位的語法
```bash=
# 透過$elemMatch 查詢內嵌欄位的資訊
db.people.find({欄位:{$elemMatch:{內部欄位1:值,內部欄位2:值}}})
db.people.find({hobby:{$elemMatch:{type:"Movie", rating:3}}})
```
### 排序
我們可以透過sort函數來將想排序的資料進行排序,sort排序是針對find出來的結果來進行。
```bash=
# 1則代表升冪、-1則代表降冪
db.movie.find().sort({排序欄位:1}).pretty()
db.movie.find().sort({imdb_score:1}).pretty()
# 多條件排序,排序完分數用年份進行排序
db.movie.find().sort({imdb_score:1,yeas:1}).pretty()
# 透過limit()顯示超過20筆資訊(預設限制)
db.movie.find().sort({imdb_score:1,yeas:1}).pretty().limit(30)
# 透過skip忽略掉前幾個(分頁用)
db.movie.find().sort({imdb_score:1,yeas:1}).pretty().limit(3).skip(3)
# 若我們今天要將limit出來的結果進行排序,可以使用aggregate()
db.movie.aggregate([{$limit:3},{$sort:{imdb_score:1}}])
```
### 更新
我們可以透過updateOne or updateMany來新增或修改一個或多個資料。
```bash=
db.people.updateOne({filter},{$set:{想更改的欄位:想更改的值}})
# 找到資料內名稱為Jack的資料(1筆),並將他的phone設為特定值
db.people.updateOne({name:"Jack"},{$set:{phone:1234}})
# 找到hobby.type內符合Movie的資料(多筆),並將他的phone設為特定值
db.people.updateMany({"hobby.type":"Movie"},{$set:{phone:8899}})
```
我們可以透過$rename來重新命名key的名稱,目前key為phone。

```bash=
db.people.updateMany({filter},{$rename:{原key:"新名稱"}})
db.people.updateMany({},{$rename:{phone:"phoneNumber"}})
```

### 刪除key
刪除key
```bash=
# 我們可以透過$unset來刪除資料中的key,刪除key同時資料也會刪除。
db.people.updateMany({filter},{$unset:{想刪除的key:1}})
db.people.updateMany({},{$unset:{phoneNumber:1}})
```
### 運算
增加的指令
```bash=
# 我們可以透過inc來增加特定欄位的值(做運算)
db.people.updateOne({filter},{$inc:{欄位:值}})
# 針對名稱為Jack的資料,將年齡增加一歲,若要扣則增加負數
db.people.updateOne({name:"Jack"},{$inc:{age:1}})
```
取最大最小並修改的操作符
```bash=
# 若filter尋找對象的值大於max所設定,則不進行更改,反之改為設定值
db.people.updateOne({name:"Jack"}, {$max:{age:35}})
# $min相同道理
db.people.updateOne({name:"Jack"}, {$min:{age:5}})
```
### 多條件查詢
多條件查詢
```bash=
db.people.find(
{
hobby:
{
$elemMatch:
{
$and:
[
{
type:"Movie"
},
{
rating:{$gte:3}
}
]
}
}
}
)
```
針對多條件查詢出來的結果進行增加值
```bash=
# 後面串接$inc,這裡要注意的是我們需要透過.$.欄位來設值。
db.people.updateMany(
{
欄位:
{
$elemMatch: {
$and: [
{內部欄位: "值"},
{ 內部欄位2: { filter } }
]
}
}
},
{
$inc: { "欄位.$.想修改的內部欄位": 1 }
}
)
# 將rating +1
db.people.updateMany({hobby:{$elemMatch:{$and:[{type:"Movie"},
{rating:{$gte:3}}]}}}, {$inc:{"hobby.$.rating":1}})
```
upsert針對未創建的資料進行創建。
```bash=
# 若我們今天update一個未存在資料庫的資訊,則資料不會被新增
db.people.updateOne({name:"Max"}, {$set:{name:"Max", age:30}})
```

```bash=
# 我們可以將upsert設為true,若資料未存在,則會自動新增
db.people.updateOne({name:"Max"}, {$set:{name:"Max", age:30}},{upsert:true})
```

### deleteMany
我們可以透過deleteMany清空所有資料,Collection還是會留著。
```bash=
# collection會留著
db.people.deleteMany({})
# collection會刪除
db.people.drop()
# 刪除整個database
db.dropDatabase()
```
## 進階操作
### Index
為什麼要用index來尋找,因為當我們使用條件搜索時,會走訪所有的資料,因此會花費大量的資源。
將collection表進行排序並建立好index的表,透過指標的指向來對應資料,查詢時我們只需要針對index做搜尋就可以以更快的方式找到資料。

#### Explain查詢效能
透過explain()我們可以知道查詢的細節,如果加入"executionStats"則可以顯示花費時間等更多詳細資訊,下圖可發現總花費22毫秒。
```bash=
db.test.explain("executionStats").find({year:{$gte:2015}})
```

#### 創建index
```bash=
# 針對特定欄位建立index,其數字1與-1差別在於升冪、降冪。
db.test.createIndex({欄位:數字})
db.test.createIndex({year:1})
```
建立完成後,再進行一次查詢可發現,查詢速度變更快了,從原本的22毫秒變為6毫秒,我們可以看到在。

index搜尋未必快,當我們搜尋大量的數據時,因為index還需要映射到collection中,所以會導致效能大幅降低。
```bash=
# 搜尋2015年以前的影片
db.test.explain("executionStats").find({year:{$lt:2015}})
```
在有Index的狀況下需要花費56秒

#### 刪除index
嘗試刪除index,再進行一次搜尋
```bash=
db.test.dropIndex({year:1})
```
僅僅花費23毫秒,比index搜尋還快。

取得Index,系統默認會採用_id當成index。
```bash=
db.people.getIndexes()
```
#### 創建一個unique的index
```bash=
# 我們可以設定name的欄位為unique,當插入新的資料為重複名稱時會插入失敗。
db.people.createIndex({name:1}, {unique: true})
```

#### TTL index
我們可以建立TTL index來將資料進行控管,設定一個TTL時間,若超時則資料庫會自動刪除,通常用在log。
```bash=
# 插入一筆資料,並建立一個欄位createAt, new Date()會存當下時間。
db.ttl.insertOne({name:"Jack",createAt: new Date()})
# 建立ttl index
db.ttl.createIndex({createAt:1}, {expireAfterSeconds: 10})
# 過10秒後搜尋會發現資料自動被刪除
db.ttl.find()
```

### compound index 聯合索引
#### 建立聯合索引
透過多個索引鍵來設定index。
```bash=
db.people.createIndex({name:1, age:1})
```
#### 聯合索引的查詢
當我們設定聯合索引後,如果是單獨針對「第一個」index進行查詢,則也會套用聯合索引的查詢方式。
```bash=
db.people.explain().find({name:"Jack"})
```
我們會發現stage還是index搜索,且index名稱是我們剛剛設定的聯合索引。

#### 聯合索引的誤區
如果單獨查詢index則會發現,並不會套用聯合索引的查詢。

我們可以透過以下指令刪除聯合索引
```bash=
db.people.dropIndex({name:1, age:1})
```
#### 字串的索引
創建字串索引
```bash=
# 針對索引字串設定text
db.movie.createIndex({title:"text"})
```
當我們要進行搜索時,搜索的值一定要是關鍵字,若不是則會無結果。
```bash=
# 搜尋單一個關鍵字結果
db.movie.find({$text:{$search: "Avengers"}})
# 搜尋多關鍵字結果,搜尋結果會出現包含關鍵字1 或 關鍵字2
db.movie.find({$text:{$search: "關鍵字1 關鍵字2"}})
# 若想搜尋一串的關鍵字則可以透過\"關鍵字\"
db.movie.find({$text:{$search:"\"The Amazing\""}})
# 希望搜尋結果符合大小寫規範可以設定$caseSensitive為true
db.movie.find({$text:{$search:"\"The Amazing\"", "$caseSensitive": true}})
# 刪除字串index,需要先透過getIndexes抓到name,再透過以下指令
db.movie.dropIndex("title_text")
# 透過設定background在後台創建index,當資料量太大的時候創建index需要大量時間
db.people.createIndex({name:1},{background:true})
```
### 地理位置存取
google map 先緯度後經度
GeoJSON 先經度後緯度
[GeoJson](https://zh.wikipedia.org/wiki/GeoJSON)專門用來存取地理訊息,Type 分為很多種,可參考網址內容,這裡我們採用的Type為Point代表一個座標位置。

```bash=
db.country.insertOne(
{
name:"ndhu",
position:
{
type:"GeoJsonType",
coordinates:[經度,緯度]
}
}
)
db.country.insertOne({name:"ndhu", position:{type:"Point", coordinates:[121.5435267,23.9416943]}})
```
#### 為GeoJSON創建Index
我們需要透過內建的"2dsphere"來創建GeoJSON的index。
```bash=
db.restaurant.createIndex({location:"2dsphere"})
```
#### 找到範圍內的所有餐廳
透過\$nearSphere、\$geometry、$maxDistance來尋找300公尺內的所有餐廳。
```bash=
# 拆解來看$nearSphere需要兩個參數,分別為$geometry與$maxDistance。
# $geometry 需要一個座標位置,包含類別與座標。
db.restaurant.find(
{
location:
{
$nearSphere:
{
$geometry:
{
type:"Point",
coordinates:[-73.9614385,40.6631119]
},
$maxDistance:300
}
}
}
)
db.restaurant.find({location:{$nearSphere:{$geometry:{type:"Point", coordinates:[-73.9614385,40.6631119]},$maxDistance:300}}})
```
#### 查看某點是否位於區域內
當我們今天創建了一個多邊形的區域(Polygon),我們可以先將資料建為index後,來查詢某一個點是否在一整個區域內。
```bash=
db.neighborhood.createIndex({geometry: "2dsphere"})
db.neighborhood.findOne(
{
geometry:
{
$geoIntersects:
{
$geometry:
{
type:"Point",
coordinates:[
121.5381638,
23.894204
]
}
}
}
}
)
```
若是返回null則代表不存在於該區域內。
### 聚合
aggregate需要傳入一個陣列,這個陣列內包含許多元素,每一個元素執行完的結果傳給下一個元素執行。
#### $match查詢
```bash=
# 我們可以單純放入一個元素來查詢符合條件的資料,這裡放入$match
db.order.aggregate(
[
{$match:{欄位:"值"}}
]
)
db.order.aggregate([{$match:{status:"finished"}}])
```

#### $group分組
```bash=
# 如果放入兩個元素,則第一個元素執行完的結果會傳給第二個元素。
# $group 代表將搜尋的結果分組為變數名稱1與變數名稱2
# $sum 則是針對特定欄位進行加總
db.order.aggregate(
[
{
$match:
{status:"finished"}
},
{
$group:
{
_id:"$欄位名稱",
變數名稱1(自訂):{$sum:"$欄位名稱"}
}
}
]
)
db.order.aggregate(
[
{
$match:{status:"finished"}
},
{
$group:
{
_id:"$name",
total:{$sum:"$amount"}
}
}
]
)
```

#### $sort排序
```bash=
# 結合上述查詢、分組再加上排序
# 將分組出來的結果進行排序
db.order.aggregate(
[
{
$match:{status:"finished"}
},
{
$group:
{
_id:"$name",
total:{$sum:"$amount"}
}
},
{
$sort:{total:-1}
}
]
)
```

求每個導演的票房總和
```bash=
db.movie.aggregate(
[
{
$group:
{
_id:"$director_name",
total:{$sum:"$gross"}
}
},
{
$sort:{total:-1}
}
]
)
```

求每個導演平均分
```bash=
db.movie.aggregate(
[
{
$group:
{
_id:"$director_name",
avg:{$avg:"$imdb_score"}
}
},
{
$sort:{avg:-1}
}
]
)
```
#### lookup
在這裡我們有兩張表,分別是product只記錄商品價格與名稱,另一張表則記錄orders,如下所示。
product

orders

如果今天我們有一個需求是在orders中,找到price of product >20的訂單,那麼我們可以透過$lookup的方式來進行尋找。
```bash=
db.product.aggregate([
{
$lookup:
{
from: "orders",
localField: "_id",
foreignField: "pid",
as: "inventory_docs"
}
}
])
```
* from: 需要與目標關聯的表,在這裡是orders
* localField: 需要關聯的鍵,在這裡是_id
* foreignField: 與目標關聯的鍵,在這裡是_pid
* as: 變數名稱
#### project
project是一種pipline技術,有先後順序之分,會依序執行前面所承接下來的結果。
透過project來定義要返回的值
```bash=
db.order.aggregate(
[
{$match:{status:"finished"}},
{$project:{name:1,_id:0}}
]
)
```

在project新增內容
```bash=
# 我們可以在project裡面自訂新變數,並指派一個陣列透過中括弧
# 內容值可以是欄位中的任何資訊
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:1,
_id:0,
自訂變數:["$欄位1","$欄位2"]
}
},
]
)
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:1,
_id:0,
content:["$amount","$name"]
}
},
]
)
```

#### 字串轉大寫
查詢結果轉大寫
```bash=
# 透過$toUpper將結果轉為大寫
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:{$toUpper:"$name"}
}
},
]
)
```
#### 字串串接
```bash=
# 透過$concat進行字串串接,其內容都必須為字串
# 透過$toString可以將數字轉為string
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:{$toUpper:"$name"},
new:{
$concat:
[
"$欄位1",
"想合併的字串",
"$欄位2"
]
}
}
},
]
)
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:{$toUpper:"$name"},
new:{
$concat:
[
"$name"," spand ",
{$toString: "$amount"}
]
}
}
},
]
)
```

#### 條件判斷
```bash=
# 透過$cond來撰寫條件
# $gt接收一個陣列,陣列包含兩個參數,前大於後回傳true
db.order.aggregate(
[
{$match:{status:"finished"}},
{
$project:
{
name:{$toUpper:"$name"},
new:
{
$concat:
[
"$name"," spand ",
{$toString: "$amount"}
]
},
flag:
{
$cond:
{
if:{$gt:["$amount",200]},
then: "big",
else: "smaill"
}
}
}
},
]
)
```

#### 取得內容數量
```bash=
# 透過$size取得欄位長度。
db.test.aggregate(
[
{
$project:{
_id:0,
name:1,
numHobby:
{$size:"$hobby"},
hobby:1
}
}
]
)
```
### 切片
```bash=
# 透過$slice進行資料切片,若是取的數量大於資料數量則取全部(不會錯)
db.test.aggregate(
[
{
$project:
{
_id:0,
name:1,
numHobby:
{
$size:"$hobby"
},
hobby:
{
$slice:["$欄位", 從第幾個開始, 取幾個]
}
}
}
]
)
db.test.aggregate(
[
{
$project:{
_id:0,
name:1,
numHobby:
{
$size:"$hobby"
},
hobby:
{
$slice:["$hobby", 0, 2]
}
}
}
]
)
```

### 分類
```bash=
# 我們可以透過bucket將資料進行分類,指定groupBy為想分類的欄位
# 將boundries設定為分類的界線,資料1:包含界線1不包含界線2
# output則是針對每個界線的資料
db.movie.aggregate(
[
{
$bucket:
{
groupBy: "$欄位",
boundaries: [界線1,界線2,界線n],
output:
{
變數1:{針對每個界線執行內容},
變數2:{針對每個界線執行內容}
}
}
}
]
)
# $sum 顯示每個分類的總和
db.movie.aggregate(
[
{
$bucket:
{
groupBy: "$year",
boundaries: [1990, 1995, 2000, 2005,
2010, 2015, 2020],
output:
{
numMoives:{$sum:1},
avgScore:{$avg:"$imdb_score"}
}
}
}
]
)
```

### 聚合中的skip & limit
```bash=
# skip與limit不能對調,如果搜尋出來的結果先limit在skip會沒有輸出。
db.movie.aggregate(
[
{
$group:
{
_id:"$director_name",
avg_imdb:{$avg:"$imdb_score"}
}
},
{
$sort:{avg_imdb:-1}
},
{
$skip:20
},
{
$limit:10
}
]
)
```

### 輸出
將查詢結果輸入到新的Collection中。
```bash=
show collections
```

```bash=
# 我們可以透過out關鍵字將結果輸出到新的collection中
db.movie.aggregate(
[
{
$group:
{
_id:"$director_name",
avg_imdb:{$avg:"$imdb_score"}
}
},
{
$sort:{avg_imdb:-1}
},
{
$out: "newCollection"
}
]
)
```
執行完畢後,我們透過`show collection`來查詢會發現多了一個collection。

我們可以透過`find()`來檢視該資料,我們會發現是剛才的結果。

### geo的聚合
操作之前需要先建立index
```bash=
db.restaurant.createIndex({location: "2dsphere"})
```
返回前10筆距離最近的餐廳
```bash=
# distanceField 設定為distance代表返回的距離的名稱為distance
# geoNear有提供num:值,用法與limit一樣,可以取代limit。
db.restaurant.aggregate(
[
{
$geoNear:
{
near:
{
type: "Point",
coordinates: [-73.8701, 40.7523]
},
maxDistance: 100000,
distanceField: "distance"
}
},
{
$project:
{
_id:0,
name:1,
distance:1
}
},
{
$limit:10
}
]
)
```

### 資料庫備份與恢復
```bash=
# 切換一個新的資料夾
# 執行mongodump即可
mongodump
```

#### 備份恢復
```bash=
# 切換到當初備份的資料夾執行以下指令即可完成
mongorestore
```

#### 針對特定Collection進行備份
```bash=
mongodump -d 資料庫 -c Collection
mongodump -d test -c movie
```
#### 備份恢復指定Collection
```bash=
mongorestore --nsInclude 資料庫.欄位 dump
mongorestore --nsInclude test.movie dump
```
#### 一鍵恢復並刪除重複
```bash=
mongorestore --drop
```

## 認證
### windows操作
windows 停止mongo
```bash=
net stop MongoDB
```
修改mongod.cfg檔案
location:`MongoDB/server/4.0/bin`
增加以下
```bash=
security:
authorization:"enable"
```
windows 啟動mongo
```bash=
net start MongoDB
mongo
```
### Mac操作
開啟認證模式,我們需要先停止mongo運行。
```bash=
# 找到進程號
ps -ef | grep mongod
kill 22764
```
重新運行以下指令,加入了`--auth`
```bash=
mongod --dbpath "/Users/chenshiyu/mongodb/data" --logpath "//Users/chenshiyu/mongodb/log/mongo.log" --fork --auth
```
加入完後,我們做了任何操作都會被視為無權限。
### 增加使用者
```bash=
use admin
db.createUser(
{
user:"admin",
pwd:"admin",
roles:[
{
role:"readWrite", db: "admin"
},
{
role: "userAdminAnyDatabase",
db: "admin"
}
]
}
)
```

### 登入用戶
當我們創建完使用者後,我們可以切換到相對應的資料庫去進行登入。
```bash=
use admin
# 登入
db.auth("admin","admin")
```

當我們切換到其他資料庫並進行資料的新增時,則會發現使用者的權限不足,因為我們當初只設定該使用者只能操作admin的資料庫。
```bash=
# 切換到未授權資料庫
use test
# 插入一筆新資料
db.new.insertOne({a:1})
```
我們會發現出現權限不足的錯誤

### Roles
#### Database User Roles
所有資料庫都遵循這些角色
* read
* readWrite
#### Database Administration Roles
所有資料庫都遵循這些角色
* dbAdmin:可以查詢資料庫,但無法增刪改
* userAdmin:可以創建用戶與權限管理
* dbOwner:可以增刪查改,擁有所有權限,包含userAdmin
#### backup & restore Roles
如果被賦予這些角色才有辦法進行backup or restore
* backup
* restore
#### All-Database Roles
被賦予的權限可以在所有資料庫執行
userAdminAnyDatabase 代表賦予userAdmin功能,並可以在任何資料庫存取
dbAdminAnyDatabase 也是同理
* readAnyDatabase
* readWriteAnyDatabase
* userAdminAnyDatabase
* dbAdminAnyDatabase
### 開始創建新用戶並進行存取
當我們透過`auth`登入完admin後,切換至要操作的資料庫。
```bash=
use test
# 創建兩個用戶,user1讀寫權限、user2只有讀取
db.createUser(
{
user:"user1",
pwd:"user1",
roles:
[
{
role:"readWrite",db:"test"
}
]
}
)
db.createUser(
{
user:"user2",
pwd:"user2",
roles:
[
{
role:"read",db:"test"
}
]
}
)
```
創建完兩個用戶之後,我們可以透過以下指令查詢所有用戶
```bash=
db.getUsers()
```

登出指令
```bash=
# 需要先回到原本的資料庫
use admin
# 執行登出
db.logout()
```

切換回test資料庫,並登入剛剛所創建的user1

```bash=
# 登入完成後我們就可以使用插入指令了
db.test.insertOne({A:1})
```

同一時間最好就只有一個用戶登入,不然會報錯。
#### 用戶的創建與更新
userAdmin才能進行資料庫使用者的更新。
```bash=
# 首先先將原本user進行登出
db.logout()
# 切換到admin資料庫
use admin
# 登入admin
db.auth("admin","admin")
# 切換到創建test的資料庫
use test
# 我們可以透過getUser("使用者名稱")來取得詳細權限
db.getUser("user1")
# 透過updateUser,這個方法會進行覆寫,因此也需要將原本的屬性添加上
db.updateUser(
"user1",
{
roles:
[
{
role: "readWrite", db: "test"
},
{
role: "userAdmin", db: "test"
}
]
}
)
```
透過剛剛創建的user1
刪除用戶
```bash=
db.dropUser("user2")
```

創建用戶
```bash=
db.createUser(
{
user:"user3",
pwd:"user3",
roles:
[
{
role:"read",db:"test"
}
]
}
)
```

#### mongoimport認證設定
```javascript=
mongoimport json的絕對路徑 -d 目標資料庫 -c 目標資料表 --jsonArray --authenticationDatabase admin --username "yourName" --password "your password"
```
## 工具
### mongoDB圖形化管理工具
[Robo 3T Download](https://robomongo.org/download)
當我們下載完後,執行安裝,會出現以下視窗,我們需要按下create創建一個連線,並在在該連線下建立Authentication

創建完連線後,便可以看到旁邊出現資料庫相關資訊

[mongoDB compass](https://www.mongodb.com/try/download/compass)
```bash=
# 我們先將admin權限放大至root
db.updateUser(
"admin",
{
roles:[
{
role: "readWrite",
db:"admin"
},
{
role: "userAdminAnyDatabase",
db:"admin"
},
{
role: "root",
db:"admin"
}
]
}
)
```
我們可以點擊`Fill in connection fields individually`來依序填入資料。

當我們成功連線進來後可以進入test中的Moive來查看。

我們可以在Filter的位置輸入相關的filter來過濾,再按下Find。
```bash=
{year:2015, imdb_score:{$gt:7}}
```

我們可以透過展開option取得更多操作方式

我們可以透過切換到index頁面來新增index

我們可以到Aggregation中新增聚合,選擇group並將參數填入

按下下方的add Stage,繼續新增條件。

## 最終章
### Replication(主從複製)

當我們運行的機器出現問題時,這時候我們需要由其他Secondary的機器來成為Primary,此時所運用的技術就是Relication,機器之間透過Heartbeat彼此監控,當資料需要同步時則透過Replication同步。
在這裡我們用一台機器模擬多台機器,透過不同port產生服務,再利用`--replSet`將他們設為同一個Set,再執行以下指令之前我們須先刪除原本的進程。
```bash=
mkdir -p ~/db/data1
mkdir -p ~/db/log1
mongod --dbpath ~/db/data1 --port 27021 --replSet set1 --fork --logpath ~/db/log1/mongodb.log
mkdir -p ~/db/data2
mkdir -p ~/db/log2
mongod --dbpath ~/db/data2 --port 27022 --replSet set1 --fork --logpath ~/db/log2/mongodb.log
mkdir -p ~/db/data3
mkdir -p ~/db/log3
mongod --dbpath ~/db/data3 --port 27023 --replSet set1 --fork --logpath ~/db/log3/mongodb.log
mkdir -p ~/db/data4
mkdir -p ~/db/log4
mongod --dbpath ~/db/data4 --port 27024 --replSet set1 --fork --logpath ~/db/log4/mongodb.log
```

由於我們剛剛修改了預設Port,因此在這裡我們連線時需要設定`--port 27021`
```bash=
mongo --port 27021
```
這時候我們需要進行初始化配置,讓當前節點變為primary
```bash=
rs.initiate()
```

透過以下指令來查看set狀態
```bash=
rs.status()
```
我們會發現此時的成員只有一個。

添加成員
```bash=
rs.add("localhost:27022")
rs.add("localhost:27023")
rs.add("localhost:27024")
```

查詢後會發現有四個成員
```bash=
rs.status()
```
移除節點
```bash=
# 當我們移除節點時,會將進程也一併銷毀
rs.remove("localhost:27024")
```
重新加入27024並將設定為仲裁節點,仲裁節點(arr),需要透過`rs.addArb`來添加。
```bash=
rm -rf db/data4
rm -rf db/log4
mkdir -p ~/db/data4
mkdir -p ~/db/log4
mongod --dbpath ~/db/data4 --port 27024 --replSet set1 --fork --logpath ~/db/log4/mongodb.log
# 重新連回primary節點
mongo --port 27021
rs.addArb("localhost:27024")
```

嘗試Replication
```bash=
# 切換到test並新增一筆資料
db.demo.insertOne({A:1})
# 退出(假裝出狀況),刪除27021的進程
kill 46716
# 登入其中一個節點並執行查詢status
mongo --port 27022
rs.status()
```
我們會發現,27023變為primary

我們可以離開當前mongodb切換到primary
```bash=
# 切換到primary
mongo --port 27023
# 查看資料庫資料是否在
db.demo.findOne()
```

再次啟動以前的primary會發現他已經變為Secondary
```bash=
mongod --dbpath ~/db/data1 --port 27021 --replSet set1 --fork --logpath ~/db/log1/mongodb.log
mongo --port 27021
```
## Sharding 切片
分為Vertical Scaling、Horizontal scaling
**Vertical Scaling**
只用單一一台主機,將該台主機的硬體設備弄至最優。
優點:簡單
缺點:不具有可持續性
**Horizontal scaling**
透過增加伺服器數量來分擔效能。
優點:良好擴展性
缺點:複雜資料庫軟體架構
Shard 專門存取Database資料,一個Shard可能由多個主機所組成。
Config Servers 用於存取MetaData

一個Collection可能會被切成多個Shard

### 建立一個Shard系統
首先我們必須先終止先前的所有mongo進程,並刪除所有db/*
```bash=
rm -rf ~/db/*
# 配置一台config server,將replSet名稱設定為config
mkdir -p ~/db/config/data
mkdir -p ~/db/config/log
mongod --port 27022 --dbpath ~/db/config/data --configsvr --replSet config --fork --logpath ~/db/config/log/mongodb.log
# 進入mongo27022並初始化
mongo --port 27022
rs.initiate()
# 透過mongos配置router
# 設定--configdb 為要連接的db
# configdb 指向先前設定好的replSet的名稱config
# 啟動mongos之前,最好先登入27022 & run rs.initiate()
mkdir -p ~/db/mongos/log
mongos --configdb config/localhost:27022 --port 27021 --fork --logpath ~/db/mongos/log/mongodb.log
# 創建shard1
# --shardsvr 代表為 shard
mkdir -p ~/db/shard0/data
mkdir -p ~/db/shard0/log
mongod --port 27023 --dbpath ~/db/shard0/data --shardsvr --fork --logpath ~/db/shard0/log/mongodb.log
# 創建shard2
mkdir -p ~/db/shard1/data
mkdir -p ~/db/shard1/log
mongod --port 27024 --dbpath ~/db/shard1/data --shardsvr --fork --logpath ~/db/shard1/log/mongodb.log
# 進入
mongo --port 27021
# 加入shard
sh.addShard("localhost:27023")
sh.addShard("localhost:27024")
# 查詢shard status
db.printShardingStatus()
# 更新chunk大小
# 可以把chunk想成切片的最大尺寸
use config
db.setting.save({_id:"chunksize", value:1})
# 啟動一個名為testdb
sh.enableSharding("testdb")
# 指定某個Collection進行分片,將testkey變為shard key
sh.shardCollection("testdb.testcollection", {testkey: 1})
```