Перейти к основному содержимому

Хранение файлов, проиндексированных базой данных

· 8 мин. чтения

1. Обзор

Когда мы создаем какое-то решение для управления контентом, нам нужно решить две проблемы. Нам нужно место для хранения самих файлов, и нам нужна какая-то база данных для их индексации.

Содержимое файлов можно хранить в самой базе данных, или мы можем хранить содержимое где-то еще и индексировать его в базе данных.

В этой статье мы собираемся проиллюстрировать оба этих метода с помощью базового приложения Image Archive. Мы также внедрим REST API для загрузки и выгрузки.

2. Вариант использования

Наше приложение архива изображений позволит нам загружать и скачивать изображения в формате JPEG .

Когда мы загружаем изображение, приложение создаст для него уникальный идентификатор. Затем мы можем использовать этот идентификатор для его загрузки.

Мы будем использовать реляционную базу данных с Spring Data JPA и Hibernate .

3. Хранение базы данных

Начнем с нашей базы данных.

3.1. Объект изображения

Во-первых, давайте создадим нашу сущность изображения :

@Entity
class Image {

@Id
@GeneratedValue
Long id;

@Lob
byte[] content;

String name;
// Getters and Setters
}

Поле id снабжено аннотацией @GeneratedValue . Это означает, что база данных создаст уникальный идентификатор для каждой добавляемой нами записи. Индексируя изображения с этими значениями, нам не нужно беспокоиться о нескольких загрузках одного и того же изображения, конфликтующих друг с другом.

Во-вторых, у нас есть аннотация Hibernate @Lob . Так мы сообщаем JPA о своем намерении сохранить потенциально большой двоичный файл .

3.2. Репозиторий изображений

Далее нам нужен репозиторий для подключения к базе данных .

Мы будем использовать Spring JpaRepository :

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

Теперь мы готовы сохранить наши изображения. Нам просто нужен способ загрузить их в наше приложение.

4. Контроллер REST

Мы будем использовать MultipartFile для загрузки наших изображений. Загрузка вернет imageId , который мы можем использовать для загрузки изображения позже.

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 содержит содержимое и исходное имя файла. Мы используем это для создания нашего объекта изображения для хранения в базе данных.

Этот контроллер возвращает сгенерированный идентификатор в качестве тела своего ответа.

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 содержит идентификатор, сгенерированный при загрузке. Если указан недопустимый идентификатор, мы используем ResponseStatusException для возврата кода ответа HTTP 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 "image=@foreach.jpeg" http://localhost:8080/image

Поскольку ответом службы загрузки является imageId , и это наш первый запрос, вывод будет таким:

1

5.2. Тест загрузки изображения

Затем мы можем загрузить наше изображение:

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

Параметр -o image.jpeg создаст файл с именем image.jpeg и сохранит в нем содержимое ответа:

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /image/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Accept-Ranges: bytes
< Content-Type: image/jpeg
< Content-Length: 9291

Мы получили HTTP/1.1 200, что означает, что наша загрузка прошла успешно.

Мы также можем попробовать загрузить изображение в браузере, нажав http://localhost:8080/image/1 .

6. Разделите контент и местоположение

Пока что мы можем загружать и скачивать изображения в базе данных.

Еще один хороший вариант — загрузить содержимое файла в другое место. Затем мы сохраняем только его расположение в файловой системе в БД `` .

Для этого нам нужно добавить новое поле в наш объект изображения :

String location;

Это будет содержать логический путь к файлу в каком-то внешнем хранилище. В нашем случае это будет путь в файловой системе нашего сервера.

Однако мы можем в равной степени применить эту идею к разным магазинам. Например, мы могли бы использовать облачное хранилище — Google Cloud Storage или Amazon S3 . Местоположение также может использовать формат URI, например, s3://somebucket/path/to/file .

Наша служба загрузки вместо того, чтобы записывать байты файла в базу данных, сохранит файл в соответствующей службе — в данном случае в файловой системе — и затем поместит местоположение файла в базу данных.

7. Хранилище файловой системы

Давайте добавим в наше решение возможность хранить изображения в файловой системе .

7.1. Сохранение в файловой системе

Во-первых, нам нужно сохранить наши изображения в файловой системе:

@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();
}
}

Одно важное замечание: нам нужно убедиться, что каждое из наших изображений имеет уникальное местоположение , определенное на стороне сервера во время загрузки . В противном случае наши загрузки могут перезаписать друг друга.

Это же правило применимо к любому облачному хранилищу, где мы должны создавать уникальные ключи. В этом примере мы добавим текущую дату в формате миллисекунд к имени изображения:

/workspace/archive-achive/target/classes/1602949218879-foreach.jpeg

7.2. Получение из файловой системы

Теперь давайте реализуем код для извлечения нашего изображения из файловой системы:

FileSystemResource findInFileSystem(String location) {
try {
return new FileSystemResource(Paths.get(location));
} catch (Exception e) {
// Handle access or file not found problems.
throw new RuntimeException();
}
}

Здесь мы ищем изображение, используя его местоположение . Затем мы возвращаем FileSystemResource .

Кроме того, мы перехватываем любые исключения, которые могут возникнуть при чтении нашего файла. Мы также можем генерировать исключения с определенными HTTP-статусами .

7.3. Потоковая передача данных и ресурс Spring

Наш метод findInFileSystem возвращает FileSystemResource , реализацию интерфейса ресурсов Spring . ``

Он начнет читать наш файл только тогда, когда мы его используем . В нашем случае это будет при отправке клиенту через RestController . Кроме того, он будет передавать содержимое файла из файловой системы пользователю, избавляя нас от загрузки всех байтов в память .

Этот подход является хорошим общим решением для потоковой передачи файлов клиенту. Если мы используем облачное хранилище вместо файловой системы, мы можем заменить FileSystemResource реализацией другого ресурса, например InputStreamResource или ByteArrayResource .

8. Соединение содержимого и местоположения файла

Теперь, когда у нас есть FileSystemRepository, нам нужно связать его с нашим ImageDbRepository.

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();
}
}

Сначала мы сохраняем изображение в файловой системе . Затем мы сохраняем запись, содержащую его местоположение в базе данных .

8.2. Получение из базы данных и файловой системы

Теперь давайте создадим метод для поиска нашего изображения по его идентификатору :

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

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

Сначала ищем наше изображение в базе данных . Затем мы получаем его местоположение и получаем его из файловой системы .

Если мы не находим imageId в базе данных, мы используем ResponseStatusException для возврата ответа HTTP Not Found .

9. Загрузка и загрузка файловой системы

Наконец, давайте создадим FileSystemImageController:

@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 ».

Затем мы создали маршрут загрузки, аналогичный маршруту в нашем ImageController , но без объекта dbImage .

Наконец, у нас есть маршрут загрузки, который использует FileLocationService для поиска изображения и возвращает FileSystemResource в качестве ответа HTTP.

10. Тест архива образов файловой системы

Теперь мы можем протестировать нашу версию файловой системы так же, как мы это делали с версией нашей базы данных, хотя пути теперь начинаются с « file-system »:

curl -H "Content-Type: multipart/form-data" \
-F "image=@foreach.jpeg" http://localhost:8080/file-system/image

1

И далее скачиваем:

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

11. Заключение

В этой статье мы узнали, как сохранить информацию о файле в базе данных с содержимым файла либо в той же строке, либо во внешнем расположении.

Мы также создали и протестировали REST API, используя многокомпонентную загрузку, и предоставили функцию загрузки с помощью Resource , чтобы разрешить потоковую передачу файла вызывающей стороне.

Как всегда, образцы кода можно найти на GitHub .