При создании веб-приложений часто требуется правильно управлять загрузкой файлов. После получения файлы могут храниться в нескольких местах: в файловой системе, в базе данных или, что более распространено, в облачном сервисе хранения данных.
В этой статье мы расскажем, как хранить файлы в базе данных с помощью 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 для хранения двоичных данных: TINYBLOB, BLOB и LONGBLOB. TINYBLOB подходит для хранения небольших данных с максимальным размером 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.
Обратите внимание, что данная настройка не учитывает такие сложности реального мира, такие как обработка ошибок, проверка ввода и управление файлами (например, обработка дубликатов файлов), которые необходимо реализовать перед развертыванием приложения.
Не забудьте выбрать правильный вариант хранения, который соответствует не только потребностям вашего приложения сейчас, но и вашим будущим требованиям к масштабируемости.