一文说通Dotnet操作MongoDB GridFS
補個技術債。
?
這個主題一直在列表中,今天把它補上。還有一個原因,就是網上能查到的代碼,大多已經過期了。今天寫的,是按最新的SDK做的例子。
?
一、MongoDB GridFS
先說說 GridFS。
MongoDB 是用 Bson 來存儲數據的,每一行數據,稱為 Document。每個 Document,大小有個上限,是16M,也就是說,結構化數據量大的空間占用是16M。注意,這個16M不是簡單的內容總和,因為 Bson 對于字段名和類型有一定的特殊處理,實際存儲的內容在計算上或多或少會有些變化,真正限制的是存儲 Bson 的16M。
對于超過16M的數據,比方一個圖片文件,MongoDB 也提供了一種存儲方式,就是 GridFS。
不同于常規的結構數據存儲,MongoDB 在存儲 GridFS 數據時,用了兩個集合(Collection):fs.files 和 fs.chunks。files 集合用來存儲文件的相關信息,chunks 集合用來存儲真正的文件塊。
在SDK早期,這兩個集合可以獨立操作(因為這兩個文件本身也是可操作的集合)。但實際應用中,這帶來了相當程度的混亂。因此,在SDK 2.0以后,加入了一個桶(Bucket)的概念。從此,操作GridFS的流程變成了:開發者對「桶」進行操作,而「桶」在系統內部進行對兩個集合的操作。
這就是上面說的過期代碼的問題。
?
桶(Bucket)在MongoDB 中是個概念,對應著兩個集合 files 和 chunks。桶的默認名稱是 fs,對應的兩個集合是 fs.files 和 fs.chunks。我們也可以給桶命名,例如 Test,則對應的集合會是 Test.files 和 Test.chunks。
二、操作GridFS
在操作GridFS時,我們會直接對桶(Bucket)操作。桶是建立在數據庫 Database 上的。
1. 前置操作
要使用GridFS,我們需要引入一個庫:
%?dotnet?add?package?MongoDB.Driver.GridFS這個庫是MongoDB官方的Dotnet庫。
引入這個庫時,系統會加入以下四個庫:
MongoDB.Bson
MongoDB.Driver
MongoDB.Driver.Core
MongoDB.Driver.GridFS
熟悉MongoDB開發的會清楚,前三個是結構化操作需要引入的庫。而第四個,就是 GridFS 操作的庫。
2. 打開Bucket
其實我更愿意叫連接。就是連接到一個桶的意思。
var?client?=?new?MongoClient(connection_string);; var?database?=?client.GetDatabase("TestDatabase"); var?bucket?=?new?GridFSBucket(database);三行代碼,就連接到了一個Bucket。
如果需要連接到一個指定名稱的Bucket,可以用下面的代碼:
var?bucket?=?new?GridFSBucket(database,?new?GridFSBucketOptions {BucketName?=?"Test", });3. 上傳文件
上傳文件,Bucket提供了兩個方法
UploadFromBytes
UploadFromStream
以及對應的異步方法:
UploadFromBytesAsync
UploadFromStreamAsync
此外,還提供了一組流式操作方法:
OpenUploadStream
OpenUploadStreamAsync
?
這其實就是一個簡單的IO操作:
using?(var?fs?=?new?FileStream(file_path,?FileMode.Open)) {var?id?=?await?bucket.UploadFromStreamAsync(file_name,?fs); }成功后會返回ObjectId。
方法里的file_name,對應保存到files里的文件名filename。其實它是一個標識,用來讓你查找文件用的。這個標識對應一個索引,是 { "filename" : 1, "uploadDate" : 1 }。對于相同的文件名,MongoDB視為同一個文件的多個版本,并通過uploadDate來區分版本。
?
我們來看一下保存后的數據:
Test.files
{?"_id"?:?ObjectId("60583228d37a5aec3c011557"),?"length"?:?73268,?"chunkSize"?:?261120,?"uploadDate"?:?ISODate("2021-03-22T13:59:05.278+0800"),?"md5"?:?"f2fe3c4e2828082ad9e82a11fabe6dd0",?"filename"?:?"test.jpg" }Test.chunks
{?"_id"?:?ObjectId("60583229d37a5aec3c011558"),?"files_id"?:?ObjectId("60583228d37a5aec3c011557"),?"n"?:?0,?"data"?:?BinData(0,?"/9j/...") }這是對應的兩個集合里的一條數據。上面說的返回的ObjectId,是files里的_id值。
?
實際應用時,files集合里記錄的文件信息有點少,我們需要加一些我們自己想存入的信息。這時候,可以這么寫:
var?options?=?new?GridFSUploadOptions {Metadata?=?new?BsonDocument{{?"width",?"1024"?},{?"height",?"768"?}} }; var?id?=?await?bucket.UploadFromStreamAsync(file_name,?fs,?options);我們通過Metadata,把我們自己的數據也存到了files表里。
再看一下數據:
{?"_id"?:?ObjectId("60583228d37a5aec3c011557"),?"length"?:?73268,?"chunkSize"?:?261120,?"uploadDate"?:?ISODate("2021-03-22T13:59:05.278+0800"),?"md5"?:?"f2fe3c4e2828082ad9e82a11fabe6dd0",?"filename"?:?"test.jpg",?"metadata"?:?{"width"?:?"1024",?"height"?:?"768"} }4. 下載文件
同上傳類似,一組直接方法:
DownloadAsBytes
DownloadAsBytesAsync
DownloadToStream
DownloadToStreamAsync
DownloadAsBytesByName
DownloadAsBytesByNameAsync
DownloadToStreamByName
DownloadToStreamByNameAsync
以及一組流式方法:
OpenDownloadStream
OpenDownloadStreamAsync
OpenDownloadStreamByName
OpenDownloadStreamByNameAsync
內容上跟上傳相似,多了一類用名稱查找的方法。
?
看個簡單的例子:
using?(var?fs?=?new?FileStream(save_file_name,?FileMode.Create)) {await?bucket.DownloadToStreamAsync(new?ObjectId("60583228d37a5aec3c011557")),?fs); }給出ID,就是上面保存時返回的ID值,就可以下載文件到本地。
如果需要根據文件名下載,是這樣的:
using?(var?fs?=?new?FileStream(save_file_name,?FileMode.Create)) {await?bucket.DownloadToStreamByNameAsync("test.jpg",?fs); }這樣,我們就下載到文件的最新版本了。如果想獲取文件的其它版本,可以加一個參數:
using?(var?fs?=?new?FileStream(save_file_name,?FileMode.Create)) {await?bucket.DownloadToStreamByNameAsync("test.jpg",?fs,?new?GridFSDownloadByNameOptions{Revision?=?0}); }5. 查找文件
查找文件跟結構化數據的查詢沒有區別,唯一的是引用的定義不同。
var?filter?=?Builders<GridFSFileInfo>.Filter.Eq(x?=>?x.Filename,?"test.jpg");s var?sort?=?Builders<GridFSFileInfo>.Sort.Descending(x?=>?x.UploadDateTime); var?options?=?new?GridFSFindOptions {Limit?=?1,Sort?=?sort }; using?(var?cursor?=?bucket.Find(filter,?options)) {var?fileInfo?=?cursor.ToList().FirstOrDefault(); }這個不詳細說了,一看就明白。
6. 刪除文件
也很簡單,根據ID直接刪。
刪除一個文件:
bucket.Delete(new?ObjectId("60583228d37a5aec3c011557"));或
await?bucket.DeleteAsync(new?ObjectId("60583228d37a5aec3c011557"));7. 刪除Bucket
這也是一個方法:
bucket.Drop();或
await?bucket.DropAsync();這是一次性刪除存在Bucket中的所有數據的最快方法。
三、分片
要想用好MongoDB集群,就得玩好分片。放到MongoDB集群里的GridFS,也需要分片。
不過,GridFS分片很簡單。
GridFS有兩個集合,files 和 chunks。兩個集合數據大小完全不一樣。
files 集合只包含元數據,數據占用空間不大。如果沒有特殊原因,可以不分片。如果一定要分片,用_id做片鍵就好,或者用自己存儲的信息字段,也可以。
chunks 包含文件塊,數據占用空間很大,需要分片。分片時,可以用 { files_id : 1, n : 1} 做片鍵,也可以就直接用 { files_id : 1 } 做片鍵。對于MongoDB 4.0及以上的版本,還可以用 { files_id : "hashed", n : 1 } 來做片鍵。
?
這就是今天全部的內容了。
多說兩句:我發現很多人對MongoDB有一種莫名的抗拒,只是因為MongoDB不提供大家熟悉的SQL。其實,SQL也是一種應用語言。MongoDB雖然不使用SQL,但他的寫法,也是一種很簡單的語言結構,不用特別學習的。而且,MongoDB給我的最大驚喜是他的安裝部署。MongoDB做成了一個綠色軟件。一個服務器就一個程序,程序運行,數據庫就起來了。做個集群,也只是一些簡單的配置。加個帳號密碼,就相當的安全了。這是多么爽的事啊?
喜歡就來個三連,讓更多人因你而受益
總結
以上是生活随笔為你收集整理的一文说通Dotnet操作MongoDB GridFS的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WPF 动态更换图片路径
- 下一篇: 老刘在微软Ignite China大会上