1. 概述

在构建内容管理系统时,通常会面临两个核心问题:文件本身要存哪里,以及如何通过数据库进行索引管理。

一种做法是直接把文件内容存进数据库;另一种更常见的做法是将文件内容存放在外部存储系统中,而数据库只保存其元信息和访问路径。

本文将以一个简单的图像归档应用为例,演示这两种文件存储方式的实现,并提供对应的 REST API 实现上传与下载功能。

2. 使用场景

我们的图像归档应用支持 JPEG 图像的上传和下载

上传时,系统会为每张图片生成唯一标识符(ID),后续可通过该 ID 下载对应图片。

我们将使用关系型数据库,并结合 Spring Data JPAHibernate 来实现持久化操作。

3. 数据库存储方案

3.1. 图像实体类

首先定义 Image 实体类:

@Entity
class Image {

    @Id
    @GeneratedValue
    Long id;

    @Lob
    byte[] content;

    String name;
    // Getters and Setters
}
  • @Id + @GeneratedValue:由数据库自动生成主键。
  • @Lob:告诉 JPA 这是一个可能很大的二进制字段。

3.2. 图像 Repository

接下来创建用于数据库交互的 Repository:

@Repository
interface ImageDbRepository extends JpaRepository<Image, Long> {}

现在我们已经可以保存图像了,下一步就是提供上传接口。

4. REST 控制器

我们使用 MultipartFile 处理文件上传,上传成功后返回图像 ID。

4.1. 图像上传

创建 ImageController 支持上传:

@RestController
class ImageController {

    @Autowired
    ImageDbRepository imageDbRepository;

    @PostMapping
    Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
        Image dbImage = new Image();
        dbImage.setName(multipartImage.getName());
        dbImage.setContent(multipartImage.getBytes());

        return imageDbRepository.save(dbImage)
            .getId();
    }
}

上传逻辑很简单:

  • MultipartFile 中提取文件名和字节流
  • 构造 Image 对象并保存到数据库
  • 返回生成的 ID

4.2. 图像下载

添加下载接口:

@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {
    byte[] image = imageRepository.findById(imageId)
      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))
      .getContent();

    return new ByteArrayResource(image);
}
  • 根据 imageId 查询数据库
  • 若未找到则抛出 404 异常
  • 否则返回封装为 ByteArrayResource 的字节流

5. 测试数据库存储方案

打包并启动应用:

mvn package
java -jar target/image-archive-0.0.1-SNAPSHOT.jar

5.1. 上传测试

使用 curl 上传图像:

curl -H "Content-Type: multipart/form-data" \
  -F "[email protected]" http://localhost:8080/image

响应结果为:

1

5.2. 下载测试

使用 curl 下载图像:

curl -v http://localhost:8080/image/1 -o image.jpeg

输出示例:

< HTTP/1.1 200 
< Content-Type: image/jpeg
< Content-Length: 9291

浏览器访问地址:http://localhost:8080/image/1

✅ 成功下载说明一切正常。

6. 分离内容与位置

前面的做法是将整个文件内容都存在数据库里,其实这不是最优解。

更好的方式是将文件内容存放在外部存储(如文件系统、云存储等),数据库仅记录其访问路径。

为此我们需要在 Image 实体中新增字段:

String location;

这个字段表示文件在外部存储中的逻辑路径,比如本地文件系统的绝对路径或者 S3 的 URI。

7. 文件系统存储实现

7.1. 文件写入

创建 FileSystemRepository 类来处理文件存储:

@Repository
class FileSystemRepository {

    String RESOURCES_DIR = FileSystemRepository.class.getResource("/")
        .getPath();

    String save(byte[] content, String imageName) throws Exception {
        Path newFile = Paths.get(RESOURCES_DIR + new Date().getTime() + "-" + imageName);
        Files.createDirectories(newFile.getParent());

        Files.write(newFile, content);

        return newFile.toAbsolutePath()
            .toString();
    }
}

⚠️ 注意:为了避免文件名冲突,我们在文件名前加上时间戳保证唯一性。

7.2. 文件读取

读取文件内容的方法如下:

FileSystemResource findInFileSystem(String location) {
    try {
        return new FileSystemResource(Paths.get(location));
    } catch (Exception e) {
        throw new RuntimeException();
    }
}

返回的是 Spring 的 FileSystemResource,它支持延迟加载和流式传输。

7.3. Spring Resource 机制

FileSystemResource 是 Spring 提供的 Resource 接口的一个实现。

它的优势在于:

  • ✅ 延迟加载:只有在真正需要时才开始读取文件
  • ✅ 流式传输:避免一次性将大文件加载进内存

如果将来要迁移到云存储(如 AWS S3、GCS),只需替换为相应的 Resource 实现即可,例如 InputStreamResourceByteArrayResource

8. 链接数据库与文件系统

现在我们要整合数据库和文件系统两部分。

8.1. 存储流程整合

创建 FileLocationService 服务类:

@Service
class FileLocationService {

    @Autowired
    FileSystemRepository fileSystemRepository;
    @Autowired
    ImageDbRepository imageDbRepository;

    Long save(byte[] bytes, String imageName) throws Exception {
        String location = fileSystemRepository.save(bytes, imageName);

        return imageDbRepository.save(new Image(imageName, location))
            .getId();
    }
}

流程:

  1. 将文件保存至文件系统
  2. 将文件路径存入数据库

8.2. 查询流程整合

查询方法:

FileSystemResource find(Long imageId) {
    Image image = imageDbRepository.findById(imageId)
      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

    return fileSystemRepository.findInFileSystem(image.getLocation());
}

流程:

  1. 在数据库中查找图像记录
  2. 获取其 location 字段
  3. 从文件系统中读取文件并返回

9. 文件系统上传与下载接口

创建新的 Controller:

@RestController
@RequestMapping("file-system")
class FileSystemImageController {

    @Autowired
    FileLocationService fileLocationService;

    @PostMapping("/image")
    Long uploadImage(@RequestParam MultipartFile image) throws Exception {
        return fileLocationService.save(image.getBytes(), image.getOriginalFilename());
    }

    @GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
    FileSystemResource downloadImage(@PathVariable Long imageId) throws Exception {
        return fileLocationService.find(imageId);
    }
}

接口路径以 /file-system 开头,区别于纯数据库版本。

10. 文件系统版本测试

上传:

curl -H "Content-Type: multipart/form-data" \
  -F "[email protected]" http://localhost:8080/file-system/image

1

下载:

curl -v http://localhost:8080/file-system/image/1 -o image.jpeg

✅ 同样验证无误。

11. 总结

本篇文章展示了两种文件存储策略:

  • 直接将文件内容存入数据库
  • 将文件内容存入外部存储(如文件系统),数据库仅保留路径信息

我们还构建并测试了基于 Spring Boot 的 REST API,实现了多部件上传与资源流式下载功能。

完整代码可参考 GitHub 示例项目: https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-boot-persistence-2


原始标题:Storing Files Indexed by a Database