Рубрики
Без рубрики

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

Узнайте, как хранить файлы в базе данных в виде больших объектов или как использовать базу данных в качестве индекса файловой системы.

Автор оригинала: Gilvan Ornelas.

1. Обзор

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

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

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

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 {}

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

4. Контроллер ОТДЫХА

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

4.1. Загрузка изображений

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

@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 для хранения в базе данных.

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

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

Переменная image Id path содержит идентификатор, сгенерированный при загрузке. Если указан недопустимый идентификатор, то мы используем 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 "[email protected]" http://localhost:8080/image

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

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. Раздельное содержание и расположение

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

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

Для этого нам нужно будет добавить новое поле в нашу сущность Image :

String location;

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

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

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

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-baeldung.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

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

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

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

8. Подключение содержимого и местоположения файла

Теперь, когда у нас есть репозиторий Файловой системы, нам нужно связать его с нашим репозиторием Image Db.

8.1. Сохранение в базе данных и Файловой системе

Давайте создадим Службу определения местоположения файлов , начиная с нашего потока сохранения:

@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. Извлечение из Базы данных и Файловой системы

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

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

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

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

Если мы не найдем идентификатор изображения в базе данных, мы используем исключение ResponseStatusException для возврата ответа HTTP Not Found|/.

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

Наконец, давайте создадим контроллер образа файловой системы :

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

Во-первых, мы сделали наш новый путь начинающимся с “/ файловая система “.

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

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

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. Заключение

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

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

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