• Dotnet操作MongoDB GridFS
  • 发布于 2个月前
  • 664 热度
    0 评论
这个主题一直在列表中,今天把它补上。还有一个原因,就是网上能查到的代码,大多已经过期了。今天写的,是按最新的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做成了一个绿色软件。一个服务器就一个程序,程序运行,数据库就起来了。做个集群,也只是一些简单的配置。加个帐号密码,就相当的安全了。这是多么爽的事啊?
用户评论