使用Spring Data MongoDB如何通过GridFS ObjectId获取二进制流

17

当我已经正确获取了ObjectId时,我无法弄清如何使用spring-data-mongodb及其GridFSTemplate从GridFS流式传输二进制文件。

GridFSTemplate返回GridFSResource (getResource())或GridFSFile (findX())。

我可以通过ID获取GridFSFile

// no way to get the InputStream?
GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)))

但是没有明显的方法可以获取那个GridFSFileInputStream

只有GridFSResource允许我使用InputStreamResource#getInputstream获得相应的InputStream。但是获取GridFSResource的唯一方法是通过它的filename

// no way to get GridFSResource by ID?
GridFSResource resource = gridFsTemplate.getResource("test.jpeg");
return resource.getInputStream();

在某种程度上,GridFsTemplate API 暗示文件名是唯一的 - 而它们并不是。 GridFsTemplate 实现只返回第一个元素。

现在我正在使用原生的 MongoDB API,一切又有意义了:

GridFS gridFs = new GridFs(mongo);
GridFSDBFile nativeFile = gridFs.find(blobId);
return nativeFile.getInputStream();

看起来我误解了Spring Data Mongo的GridFS抽象概念。我期望至少可以实现以下事情之一:

  • 通过其ID获取GridFSResource
  • 为我已有的GridFsFile获取GridFSResourceInputStream

我是错了还是这个Spring Data MongoDB API的这一特定部分有些奇怪?

10个回答

11

我也遇到了这个问题。而且我对GridFsTemplate被设计成这样感到非常震惊......无论如何,到目前为止,我丑陋的“解决方案”是:

public GridFsResource download(String fileId) {
    GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));

    return new GridFsResource(file, getGridFs().openDownloadStream(file.getObjectId()));
}

private GridFSBucket getGridFs() {

    MongoDatabase db = mongoDbFactory.getDb();
    return GridFSBuckets.create(db);
}
注意:您需要注入MongoDbFactory才能使其正常工作...

1
所有互联网海洋中唯一真正的解决方案,非常非常非常非常非常非常非常感谢您。 - pagurix
是的,我也是这样做的...只需从Spring项目中复制getGridFs函数并实现一个新的getResource。他们应该将其添加到原始代码中。 - PeMa
这个解决方案如何处理媒体类型?谢谢。 - InsaurraldeAP
很高兴我不是唯一一个认为Mongo的GridFS功能与Spring Data对其抽象存在令人惊讶的阻抗不匹配的人。 - M. Justin

6
这些类型有点混乱: 从 Spring GridFsTemplate 源代码 可以看出。
public getResource(String location) {

    GridFSFile file = findOne(query(whereFilename().is(location)));
    return file != null ? new GridFsResource(file, getGridFs().openDownloadStream(location)) : null;
}

有一个不太优雅的解决方案:
@Autowired
private GridFsTemplate template;

@Autowired
private GridFsOperations operations;

public InputStream loadResource(ObjectId id) throws IOException {
    GridFSFile file = template.findOne(query(where("_id").is(id)));
    GridFsResource resource = template.getResource(file.getFilename());

    GridFSFile file = operations.findOne(query(where("_id").is(id)));
    GridFsResource resource = operations.getResource(file.getFilename());
    return resource.getInputStream();
}

1
我认为你最终是通过文件名检索到文件的。 如果你有两个同名的文件,并且你需要第二个,GridFSFile对象是正确的,但是GridFsResource资源是通过名称查找的。哪一个是它? - pagurix

2

你是否考虑过在你的解决方案中使用Spring Content for Mongo来进行内容存储?

假设你正在使用Spring Boot以及Spring Data Mongo,那么它可能看起来像下面这样:

pom.xml

<dependency>
    <groupId>com.github.paulcwarren</groupId>
    <artifactId>spring-content-mongo-boot-starter</artifactId>
    <version>0.0.10</version>
</dependency>
<dependency>
    <groupId>com.github.paulcwarren</groupId>
    <artifactId>spring-content-rest-boot-starter</artifactId>
    <version>0.0.10</version>
</dependency>

请更新您的Spring Data Mongo实体,包括以下属性:

@ContentId
private String contentId;

@ContentLength 
private long contentLength = 0L;

@MimeType
private String mimeType;

添加一个商店界面:
@StoreRestResource(path="content")
public interface MongoContentStore extends ContentStore<YourEntity, String> {
}

这就是你所需的全部内容。当你的应用程序启动时,Spring Content会识别对Spring Content Mongo/REST模块的依赖关系,并注入一个GridFs的MongonContentStore存储实现以及支持完整CRUD功能的控制器实现,并将这些操作映射到底层存储接口上。 REST终点将在“/content”下可用。
例如: curl -X PUT /content/{entityId} 将创建或更新实体的图像 curl -X GET /content/{entityId} 将获取实体的图像 curl -X DELETE /content/{entityId} 将删除实体的图像

这里有一些入门指南这里。它们使用Spring Content进行文件系统操作,但模块可互换。Mongo参考指南在这里。还有一个教程视频这里

希望对您有所帮助


以上文章看起来不错。谢谢分享。假设我正在使用上述API上传PDF、Word、文本文件等,现在我想根据用户输入搜索文件内容。如果用户输入的文本存在于3个文件中,那么我想显示这3个文件。对此有什么建议吗? - Anand
是的,Spring Content有两个全文索引模块。一个是用于solr,另一个是用于elasticsearch。如果您将它们包含在类路径中并配置一个连接bean到相关服务器,那么当您添加内容时,它将被发送进行全文索引,稍后可以进行搜索。 - Paul Warren
@Anand,这个错误通常意味着您正在运行与不兼容的elasticsearch版本。Spring Content 1.0.x已经测试过与6.8.7兼容,而Spring Content 1.1.x目前已经测试过与7.8.2兼容,如果这有帮助的话? - Paul Warren
Paul- 我正在使用Spring Content MongoDB(用于存储)+ ElasticSearch(用于内容搜索),目前运行良好,现在我正在尝试添加S3作为存储选项,但是它报错“指定的Bucket不存在”,尽管我能够使用相同的AmazonS3 bean上传。参考- https://paulcwarren.github.io/spring-content/refs/release/1.2.1/s3-index.html,请建议。 - Anand
嗨@Anand,很有趣。我假设你已经设置了AWS_BUCKET环境变量。但是听起来存储桶不太符合你的预期。如果你增加S3调用的日志记录,你可以看到它正在使用哪个存储桶吗? - Paul Warren
显示剩余9条评论

2

Spring Data 2.1.0新增了getResource()的重载方法,用于返回给定GridFsFileGridFsResourceGridFsResource有一个获取InputStream的方法。因此,如果您使用的是至少这个版本的Spring Data,则可以通过对GridFsTemplate进行两次调用来获取InputStream

GridFSFile gridFsFile =
        gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));

// In real code, perform necessary null checks in case the file doesn't exist

GridFsResource resource = gridFsTemplate.getResource(gridFsFile);
InputStream inputStream = resource.getInputStream();

1
我发现了解决这个问题的方法!只需将GridFSFile包装在GridFsResource中即可!这是为使用GridFSFile而设计的。
public GridFsResource getUploadedFileResource(String id) {
    var file = this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
    return new GridFsResource(file);
}

@GetMapping("/{userId}/files/{id}")
public ResponseEntity<InputStreamResource> getUploadedFile(
    @PathVariable Long userId,
    @PathVariable String id
){
    var user = userService
        .getCurrentUser()
        .orElseThrow(EntityNotFoundException::new);

    var resource = userService.getUploadedFileResource(id);

    try {
        return ResponseEntity
            .ok()
            .contentType(MediaType.parseMediaType(resource.getContentType()))
            .contentLength(resource.contentLength())
            .body(resource);
    } catch (IOException e) {
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }


}

这样做的巨大优势是,你可以直接将GridFsResource传递给ResponseEntity,因为GridFsResource扩展了InputStreamResource。
希望这能帮到你!
问候 Niklas

0
GridFsTemplate的getResource(com.mongodb.client.gridfs.model.GridFSFile file)函数返回GridFSFile的GridFsResource。
GridFSFile gridfsFile= gridFsTemplate.findOne(new 
Query(Criteria.where("filename").is(fileName)));
GridFsResource gridFSResource= gridFsTemplate.getResource(gridfsFile);
InputStream inputStream= gridFSResource.getInputStream();

如果以上代码在某些更高版本的Spring Boot中无法运行,请使用以下代码:
GridFSFile gridfsFile= gridFsTemplate.findOne(new 
Query(Criteria.where("filename").is(fileName)));
//or
GridFSFile  gridfsFile = 
gridFsOperations.findOne(Query.query(Criteria.where("filename").is(fileName)));
 return ResponseEntity.ok()
                .contentLength(gridFsdbFile.getLength())
                .contentType(MediaType.valueOf("image/png"))
                .body(gridFsOperations.getResource(gridFsdbFile));

0
@RequestMapping(value = "/api ")
public class AttachmentController {

private final GridFsOperations gridFsOperations;

@Autowired
public AttachmentController(GridFsOperations gridFsOperations) {
    this.gridFsOperations = gridFsOperations;
}

@GetMapping("/file/{fileId}")
public ResponseEntity<Resource> getFile(@PathVariable String fileId) {
GridFSFile file = 
gridFsOperations.findOne(Query.query(Criteria.where("_id").is(fileId)));

    return ResponseEntity.ok()
            .contentLength(file.getLength())
            .body(gridFsOperations.getResource(file));
}

0

虽然这是一个老问题,但我在2019年使用WebFlux尝试做到这一点时,必须执行以下操作

  public Mono<GridFsResource> getImageFromDatabase(final String id) {

    return Mono.fromCallable(
        () ->
            this.gridFsTemplate.getResource(
                Objects.requireNonNull(
                        this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id))))
                    .getFilename()));
  }

这将为您提供一个Mono,可以在控制器中返回。不过我相信还有更好的解决方案。


0
将GridFSFile包装在一个GridFsResource中,或者使用这个。
GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
GridFsResource resource = gridFsTemplate.getResource(file);
return resource.getInputStream();

-3
GridFSDBFile file = ... 
ByteArrayOutputStream baos = new ByteArrayOutputStream();
file.writeTo(baos);
byte[] ba = baos.toByteArray()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接