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

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

Введение

Недавно один из разработчиков, которому я помогал в прошлом, обратился ко мне в Slack за помощью с обработкой загрузки файлов в приложении Spring Boot. Предоставив некоторые рекомендации, я решил подготовить эту статью, чтобы помочь другим, столкнувшимся с подобными проблемами.

При работе с файловыми данными одним из вариантов является хранение этих файлов в базе данных в виде двоичных объектов (Binary Large Objects, или BLOBs). Однако важно понимать, что такой подход имеет свои недостатки, включая потенциально значительное влияние на производительность. Поэтому, если ваш проект имеет дело с большими файлами или большим количеством файлов, рекомендуется использовать специальную систему хранения файлов.

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

Обзор процесса хранения файлов в базе данных

Шаг 1: Класс сущности

Для начала мы создаем класс сущности. Этот класс представляет данные, которые мы будем хранить в базе данных. Пример сущности Document с полями name, type и data может выглядеть следующим образом:

public class Document {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String type;

    @Lob
    @Column(length = Integer.MAX_VALUE)
    private byte[] data;

    // constructors
    public Document() {}

    public Document(String name, String type, byte[] data) {
        this.name = name;
        this.type = type;
        this.data = data;
    }

    // getters and setters
}

В этом классе аннотация @Lob означает, что атрибут data должен храниться в базе данных как BLOB.

Соответствующая таблица MySQL для класса сущностей Document будет выглядеть следующим образом:

create table files.document
(
    id   bigint auto_increment
        primary key,
    name varchar(255) null,
    type varchar(255) null,
    data longblob     null
);

data longblob: создается столбец данных с использованием типа данных LONGBLOB, который может содержать BLOB (большой двоичный объект) данных размером до 4 ГБ.

MySQL предоставляет три основных типа данных BLOB для хранения двоичных данных: TINYBLOBBLOB и LONGBLOBTINYBLOB подходит для хранения небольших данных с максимальным размером 255 байт. BLOB увеличивает этот размер до 64 КБ, что позволяет хранить двоичные данные среднего размера, такие как изображения. LONGBLOB, максимальный размер которого составляет 4 ГБ, используется для больших двоичных файлов, таких как видео.

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

В классе Document я добавил аннотацию @Column к переменной data и установил ее длину в Integer.MAX_VALUE, что является максимальным пределом в Java для целого числа и обычно достаточным объемом памяти для поля массива байтов.

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

Шаг 2: Класс репозитория


Далее мы создаем интерфейс репозитория, расширяющий JpaRepository. Это дает нам множество стандартных методов для операций CRUD, которые мы можем использовать с нашими сущностями Document.

public interface DocumentRepository extends JpaRepository<Document, Long> {
}

Шаг 3: Класс сервиса

@Slf4j
@Service
@RequiredArgsConstructor
public class FileUploadService {

    private final DocumentRepository documentRepository;

    public void saveFileInDatabase(MultipartFile file) throws IOException {
        Document doc = new Document(file.getOriginalFilename(), file.getContentType(), file.getBytes());
        documentRepository.save(doc);
    }

}

Шаг 4: Класс контроллера

@Slf4j
@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileUploadController {
    private final FileUploadService fileUploadService;

    @PostMapping(value = "/upload", consumes = "multipart/form-data")
    public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        log.info("File name: {}", file.getOriginalFilename());

        fileUploadService.saveFileInDatabase(file);

        return "File uploaded successfully";
    }
}

Когда пользователь отправляет HTTP-запрос POST для загрузки файла, запускается метод uploadFile, который создает новый объект Document с информацией и данными файла.

Затем он использует метод fileUploadService.saveFileInDatabase(file)для сохранения этого объекта в базе данных.

Альтернативы хранению файлов в базе данных

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

Локальные или сетевые файловые системы 

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

Вот как может выглядеть сохранение файла в локальной файловой системе

public void saveFileInFileSystem(MultipartFile file) throws IOException {
    log.info("Uploading file to local file system: {}", file.getOriginalFilename());

    if (!Files.exists(rootPath)) {
        Files.createDirectories(rootPath);
    }

    try (InputStream inputStream = file.getInputStream()) {
        String filenameWithExtension = Paths.get(file.getOriginalFilename()).getFileName().toString();
        Path path = rootPath.resolve(filenameWithExtension);
        Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING);
    }
}

Теперь запустите приложение и выполните Curl:

curl -X POST -H 'Content-Type: multipart/form-data' -F 'file=@/home/uses/uploads/_cd03deb1-489d-4867-9b5b-2ffde99a3e20.jpeg http://localhost:8080/files/upload

Сервисы облачного хранения данных

Такие сервисы, как Amazon S3, Google Cloud Storage и Azure Blob Storage, предназначены для хранения и извлечения любых объемов данных из любого места.

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

public void uploadFileToS3(MultipartFile multipartFile) throws IOException {
    log.info("Uploading file to s3: {}", multipartFile.getOriginalFilename());
    var s3Client = getS3Client();

    var metadata = new ObjectMetadata();
    metadata.setContentLength(multipartFile.getSize());
    var keyName = buildKeyName(multipartFile);
    var results = s3Client.putObject(bucketName, keyName, multipartFile.getInputStream(), metadata);
    if (results != null && StringUtils.isNotBlank(results.getContentMd5())) {
        log.info("File uploaded successfully to s3: {}", multipartFile.getOriginalFilename());
    } else {
        log.error("Failed to upload file to s3: {}", multipartFile.getOriginalFilename());
        throw new RuntimeException("Failed to upload file to s3");
    }
}

Хранилище сети доставки контента (CDN)

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

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

Объектное хранилище

Объектное хранилище представляет собой оптимизированное решение для хранения больших объемов данных, каждый из которых представляет собой отдельную единицу или «объект». Amazon S3 и Google Cloud Storage являются примерами сервисов хранения объектов.

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

Такая конструкция обеспечивает высокую масштабируемость и экономическую эффективность при работе с огромными объемами неструктурированных данных.

Блочное хранилище

При блочном хранении данные делятся на стандартизированные фрагменты, называемые «блоками», каждый из которых имеет свой собственный адрес, но не содержит каких-либо дополнительных метаданных.

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

Популярные поставщики блочных хранилищ включают Amazon EBS и Google Persistent Disk.

Распределенные файловые системы (например, Hadoop HDFS, GlusterFS)

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

Распределенные файловые системы могут обрабатывать огромные объемы данных и отличаются высокой отказоустойчивостью. Однако настройка и управление такими системами могут быть достаточно сложными.

Управляемые сервисы хранения файлов (например, Google Drive API, Dropbox API)

Эти сервисы обеспечивают встроенное хранилище файлов, их организацию и безопасность. Доступ к ним можно получить через API-интерфейсы, что позволяет легко интегрировать их в ваше приложение.

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

Плюсы и минусы

Хранение файлов в базе данных обеспечивает согласованность и простоту, но может привести к проблемам с производительностью и масштабируемостью. Локальные и сетевые файловые системы обеспечивают преимущества в производительности, но имеют проблемы с масштабируемостью и целостностью данных. Облачные сервисы хранения данных обеспечивают масштабируемость и производительность, но могут быть дорогостоящими для небольших приложений. Сети доставки контента (CDN) повышают производительность, но обходятся недешево.

Объектное хранилище обеспечивает масштабируемость и доступность, но может иметь недостаточную производительность, в то время как блочное хранилище обеспечивает производительность по более высокой цене. Распределенные файловые системы обеспечивают масштабируемость и отказоустойчивость, но сложны в управлении. Управляемые сервисы хранения файлов, такие как Google Drive и Dropbox, отличаются простотой использования и безопасностью, но связаны с зависимостью от сторонних провайдеров.

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

Заключение

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

Я не стал приводить примеры кода для всех альтернатив, так как эта статья получилась бы громоздкой и во многом зависит от API и SDK этих сервисов, поэтому рекомендуется ознакомиться с их документацией.

Полный код практического примера, демонстрирующий подход к хранению файлов в базе данных, вы можете найти в репозитории fileupload-demo на GitHub.

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

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

Комментарии (0)