Как сохранить PDF-файл (или другой бинарный файл) в PostgreSQL?

«Как сохранить PDF-файл (или другой бинарный файл) в PostgreSQL?» — вопрос из категории Базы данных, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, PostgreSQL предоставляет два основных способа хранения бинарных данных (BLOB): тип bytea и механизм Large Objects.

1. Использование типа bytea (рекомендуется для файлов до 1 ГБ)

Тип bytea хранит бинарные данные как массив байтов прямо в строке таблицы.

Создание таблицы:

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    file_name VARCHAR(255) NOT NULL,
    mime_type VARCHAR(100),
    content BYTEA NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Сохранение файла из Java (JDBC):

import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.PreparedStatement;

public void savePdf(Connection conn, String filePath) throws Exception {
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String sql = "INSERT INTO documents (file_name, mime_type, content) VALUES (?, ?, ?)";

    try (PreparedStatement stmt = conn.prepareStatement(sql)) {
        stmt.setString(1, "document.pdf");
        stmt.setString(2, "application/pdf");
        stmt.setBytes(3, fileBytes); // Ключевой метод setBytes() для bytea
        stmt.executeUpdate();
    }
}

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

2. Использование Large Objects (LOB) (для файлов > 1 ГБ или потоковой обработки)

Large Objects хранят данные отдельно от таблицы, а в таблице сохраняется только OID (идентификатор объекта).

Сохранение с использованием Large Object API JDBC:

import org.postgresql.PGConnection;
import org.postgresql.largeobject.*;
import java.io.FileInputStream;
import java.sql.Connection;

public long savePdfAsLargeObject(Connection conn, String filePath) throws Exception {
    // Приведение к PGConnection для доступа к LOB API
    PGConnection pgConn = conn.unwrap(PGConnection.class);
    LargeObjectManager lom = pgConn.getLargeObjectAPI();

    // Создание нового Large Object в режиме записи
    long oid = lom.createLO(LargeObjectManager.READ | LargeObjectManager.WRITE);

    try (LargeObject lob = lom.open(oid, LargeObjectManager.WRITE);
         FileInputStream fis = new FileInputStream(filePath)) {

        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            lob.write(buffer, 0, bytesRead); // Потоковая запись
        }
    }
    return oid; // Этот OID нужно сохранить в таблице
}

Плюсы Large Objects: Эффективная потоковая передача, не требует загрузки всего файла в память, лучше для очень больших файлов. Минусы: Более сложный API, требует специфичного для PostgreSQL кода.

Рекомендация: Для большинства случаев (файлы до нескольких сотен мегабайт) используйте bytea. Для хранения очень больших файлов или необходимости потоковой обработки выбирайте Large Objects.