GridFS 是 MongoDB 中存储和查询超过 BSON 文件大小限制(16M)的规范,不像 BSON 文件那样在一个单独的文档中存储文件,GridFS 将文件分成多个块,每个块作为一个单独的文档。默认情况下,每个 GridFS 块是 255kB,意味着除了最后一个块之外(根据剩余的文件大小),文档被分成多个 255kB 大小的块存储。
GridFS 使用两个集合保存数据,一个集合存储文件块,另外一个存储文件元数据。
当从 GridFS 中获取文件时,MongoDB 的驱动程序负责将多个块组装成完整文件,你可以通过 GridFS 进行范围查询,可以访问文件的任意部分(例如跳到视频文件或者音频文件的任意位置)。
无论是超过 16M 的文件和其他文件,只要存在访问时不想加载整个文件的场景存在,GridFS 对你就有帮助。
在 MongoDB 中,使用 GridFS 存储超过 16M 的文件(BSON 文件不能超过 16M)。在某些情况下,MongoDB 存储大文件会比操作系统的文件系统更高效:
(1)如果你的文件系统限制目录下文件的个数,可以使用 MongoDB 在目录下存储任意多的文件。
(2)访问大数据文件时,不想一次加载而是分段访问。
(3)在多个系统间实现文件和元数据同步。
对文件进行原子更新时,MongoDB 不适合,不能支持对文件多个块更新操作的原子性;如果确有需要,也可以通过在元数据中指定当前版本来变通实现。
如果你的文件都小于 16M,应该考虑使用每个文件存一个独立文档的方式来取代 GridFS,可以使用 BinData 类型来存储二进制数据(也可以使用 GridFS,需要修改 chunk 大小,避免小文件被拆分,需要进行测试和比较性能)。
mongofiles 是一个 MongoDB 内建工具,可以用它在 GridFS 中上传、下载、查看、搜索和删除文件。注意,在 MongoDB 5+ 版本后 mongofiles 命令被单独出来,没有放在 MongoDB 安装包的 bin 目录中,工具下载地址 https://www.mongodb.com/try/download/database-tools(mongosh-1.9.0-win32-x64.zip)。
下面将举例说明 mongofiles 工具的用法,如下:
上传一个文件到 GridFS 文件系统中,命令格式 “mongofiles -d 数据库名字 -l "要上传的文件的完整路径名" put "上传后的文件名"”。如下:
# 将 D:\mongodb-database-tools-windows-x86_64-100.7.0\bin\README.md 文件上传到 GridFS,重命名为 readme.md D:\mongodb-database-tools-windows-x86_64-100.7.0\bin> mongofiles -d test -l D:\mongodb-database-tools-windows-x86_64-100.7.0\bin\README.md put readme.md 2023-05-18T16:05:59.754+0800 connected to: mongodb://localhost/ 2023-05-18T16:05:59.802+0800 adding gridFile: readme.md 2023-05-18T16:05:59.861+0800 added gridFile: readme.md
然后,在 MongoDB 数据库中就会多出 2 个集合,它们存储了 GridFS 文件系统的所有文件信息,查询这两个集合就能看到上传的那个文件的一些信息,如下图:
从 GridFS 文件系统中下载一个文件到本地,命令格式 “mongofiles -d 数据库名字 -l "将文件保存在本地的完整路径名" get "GridFS文件系统中的文件名"”,如果不写 -l 以及后面的路径参数,则保存到当前位置。例如:
# 将 readme.md 下载到 D:\readme.md D:\mongodb-database-tools-windows-x86_64-100.7.0\bin> mongofiles -d test -l D:\readme.md get readme.md 2023-05-18T16:11:12.023+0800 connected to: mongodb://localhost/ 2023-05-18T16:11:12.072+0800 finished writing to D:\readme.md # 将 readme.md 下载到当前目录 D:\mongodb-database-tools-windows-x86_64-100.7.0\bin> mongofiles -d test get readme.md 2023-05-18T16:11:39.710+0800 connected to: mongodb://localhost/ 2023-05-18T16:11:39.761+0800 finished writing to readme.md
查看 GridFS 文件系统中所有文件,命令格式 “mongofiles -d 数据库名字 list”,如下:
# 查看 test 数据库下面的所有文件 D:\mongodb-database-tools-windows-x86_64-100.7.0\bin> mongofiles -d test list 2023-05-18T16:12:15.790+0800 connected to: mongodb://localhost/ readme.md 3120
删除 GridFS 文件系统中的某个文件,命令格式 “mongofiles -d 数据库名字 delete "文件名"”,如下:
# 删除名为 readme.md 的文件 D:\mongodb-database-tools-windows-x86_64-100.7.0\bin> mongofiles -d test delete readme.md 2023-05-18T16:12:45.188+0800 connected to: mongodb://localhost/ 2023-05-18T16:12:45.251+0800 successfully deleted all instances of 'readme.md' from GridFS
前面介绍了 MongoDB 支持在其文件系统 GridFS 中存储二进制文件。Spring Data MongoDB 提供了一个 GridFsOperations 接口以及相应的实现,即 GridFsTemplate,让你与文件系统互动。
你可以通过给它一个 MongoDatabaseFactory 以及一个 MongoConverter 来设置一个 GridFsTemplate 实例,如下例所示:
public class GridFsConfiguration extends AbstractMongoClientConfiguration { // ...further configuration omitted @Bean public GridFsTemplate gridFsTemplate() { return new GridFsTemplate(mongoDbFactory(), mappingMongoConverter()); } }
基于 XML 配置文件实例化 GridFsTemplate,如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <mongo:db-factory id="mongoDbFactory" dbname="database" /> <mongo:mapping-converter id="converter" /> <bean class="org.springframework.data.mongodb.gridfs.GridFsTemplate"> <constructor-arg ref="mongoDbFactory" /> <constructor-arg ref="converter" /> </bean> </beans>
现在,GridFsTemplate 模板则可以被注入,并用于执行存储和检索操作,例如使用 GridFsTemplate 来存储文件,代码如下:
public class GridFsClient { @Autowired private GridFsOperations operations; @Test public void storeFileToGridFs() { FileMetadata metadata = new FileMetadata(); // populate metadata Resource file = ... // lookup File or Resource operations.store(file.getInputStream(), "filename.txt", metadata); } }
store(...) 操作接收一个 InputStream、一个文件名和(可选择的)关于要存储的文件的元数据信息。元数据可以是一个任意的对象,它将被配置在 GridFsTemplate 中的MongoConverter 处理。另外,你也可以提供一个 Document。
你可以通过 find(...) 或 getResources(...) 方法从文件系统中读取文件。让我们先看一下 find(...) 方法,你可以找到一个文件,也可以找到符合查询条件的多个文件。你可以使用 GridFsCriteria 辅助类来定义查询。它提供了静态的工厂方法来封装默认的元数据字段(比如 whereFilename() 和 whereContentType()),或者通过whereMetaData() 封装一个自定义的元数据。下面的例子显示了如何使用 GridFsTemplate 来查询文件:
public class GridFsClient { @Autowired private GridFsOperations operations; @Test public void findFilesInGridFs() { GridFSFindIterable result = operations.find(query(whereFilename().is("filename.txt"))) } }
注意:目前,MongoDB 不支持在从 GridFS 检索文件时定义排序标准。出于这个原因,任何定义在查询实例上的排序标准都会被忽略,而这些排序标准会被交给 find(...) 方法来处理。
从 GridFs 读取文件的另一个选择是使用 ResourcePatternResolver 接口引入的方法。它们允许在方法中输入 Ant 路径,因此可以检索到与给定模式相匹配的文件。下面的例子显示了如何使用 GridFsTemplate 来读取文件:
public class GridFsClient { @Autowired private GridFsOperations operations; @Test public void readFilesFromGridFs() { GridFsResources[] txtFiles = operations.getResources("*.txt"); } }
注意:GridFsOperations 扩展了 ResourcePatternResolver,并允许将 GridFsTemplate(实例)插入 ApplicationContext,以从 MongoDB 数据库读取 Spring 配置文件。
(1)application.properties 配置文件
# Log logging.level.root=info # MongoDB spring.data.mongodb.uri=mongodb://localhost:27017/test
(2)配置类,配置 GridFsTemplate 实例
package com.hxstrive.springdata.mongodb.config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.gridfs.GridFsTemplate; /** * MongoDB 配置 * @author hxstrive.com 2022/12/23 */ @Slf4j @Configuration public class AppConfig { @Bean public GridFsTemplate gridFsTemplate(MongoDatabaseFactory mongoDatabaseFactory, MongoConverter mongoConverter) { return new GridFsTemplate(mongoDatabaseFactory, mongoConverter); } }
(3)客户端代码
import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.GridFsOperations; import org.springframework.data.mongodb.gridfs.GridFsResource; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @SpringBootTest public class GridfsDemo { @Autowired private GridFsOperations operations; /** * 存入文件到 GridFS */ @Test void store() throws Exception { // 文件 FileInputStream inputStream = new FileInputStream(new File("D:\\readme.txt")); ObjectId id = operations.store(inputStream, "myFile.txt"); System.out.println(id); // 结果: // 646707cc9d59344f3fa58c21 } /** * 根据文件名获取文件信息 */ @Test void getResource() throws Exception { GridFsResource resource = operations.getResource("myFile.txt"); System.out.println("ID:" + resource.getId()); System.out.println("文件名:" + resource.getFilename()); System.out.println("大小:" + resource.contentLength()); System.out.println("正文:"); InputStream inputStream = resource.getContent(); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1) { output.write(bytes, 0, len); } inputStream.close(); System.out.println(new String(output.toByteArray())); // 结果: //ID:BsonObjectId{value=646707cc9d59344f3fa58c21} //文件名:myFile.txt //大小:21 //正文: //hello mongodb //gridfs } /** * 根据模式检索文件 */ @Test void getResources() { GridFsResource[] resources = operations.getResources("*.txt"); for(GridFsResource resource : resources) { System.out.println(resource.getFileId() + " " + resource.getFilename()); } // 结果: // 646707cc9d59344f3fa58c21 myFile.txt } /** * 根据文件名删除文件 */ @Test void delete() { operations.delete(Query.query(Criteria.where("filename").is("myFile.txt"))); } }