1. 概述

图片、音频、视频、文档等文件可以作为二进制大对象(BLOB)存储在数据库中。BLOB是一种SQL数据类型,能将大型二进制数据作为单个实体存储。

本教程将学习如何使用Java数据库连接(JDBC)和H2内存数据库存储和检索BLOB数据。

2. 示例环境搭建

首先,在pom.xml中添加H2依赖:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

接着创建名为warehouses的数据库表:

String sql = """
    CREATE TABLE IF NOT EXISTS warehouses (
    id INTEGER PRIMARY KEY,
    name text NOT NULL,
    capacity REAL,
    picture BLOB
);""";

这里创建了一个包含四列的warehouses表,其中第四列是BLOB类型,适合存储图片、PDF等二进制对象。

然后设置JDBC连接:

static Connection connect() throws SQLException {
    Connection connection = DriverManager.getConnection("jdbc:h2:./test", "sa", "");
    return connection;
}

此方法创建Connection对象建立数据库连接。

最后执行建表操作:

try (Connection connection = connect(); Statement stmt = connection.createStatement()) {
    stmt.execute(sql);
}

上述代码执行SQL创建表结构。

3. 将文件存储为BLOB

可通过两种方式存储文件内容:先转换为字节数组再插入,或分块流式传输文件内容。

3.1. 将文件转换为字节数组

对于小文件,转换为字节数组可能是高效选择。但此方法不适合超大文件。

首先编写文件转字节数组的方法:

static byte[] convertFileToByteArray(String filePath) throws IOException {
    File file = new File(filePath);
    try (FileInputStream fileInputStream = new FileInputStream(file); 
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        byte[] buffer = new byte[1024];
        for (int len; (len = fileInputStream.read(buffer)) != -1; ) {
            byteArrayOutputStream.write(buffer, 0, len);
        }
        return byteArrayOutputStream.toByteArray();
    }
}

该方法接收文件路径参数,创建FileInputStreamByteArrayOutputStream对象分别读取文件和存储字节。每次读取1024字节块,当fileInputStream返回-1时表示文件结束。

接下来向warehouses表插入新记录:

boolean insertFile(int id, String name, int capacity, String picture) throws SQLException, IOException {
    String insertSql = """
        INSERT INTO warehouses(id,name,capacity,picture) VALUES(?,?,?,?)
        """;
    try (Connection conn = connect()) {
        if (conn != null) {
            PreparedStatement stmt = conn.prepareStatement(insertSql);
            stmt.setInt(1, id);
            stmt.setString(2, name);
            stmt.setDouble(3, capacity);
            stmt.setBytes(4, convertFileToByteArray(picture));
            stmt.executeUpdate();
            return true;
        }
    }
    return false;
}

这里建立数据库连接,创建PreparedStatement对象设置各列值。文件被转换为字节数组,通过setBytes()方法设为BLOB数据。

单元测试演示插入操作:

@ParameterizedTest
@CsvSource({ "1, 'Liu', 3000", "2, 'Walmart', 5000" })
void givenBlobFile_whenInsertingTheBlobFileAsByteArray_thenSuccessful(
    int id, 
    String name, 
    int capacity
) throws SQLException, IOException {
    boolean result = jdbcConnection.insertFile(id, name, capacity, TEST_FILE_PATH);
    assertTrue(result);
}

上述代码向warehouses表添加两条记录,最后断言操作返回true。

3.2. 以流方式保存文件

处理大文件时,先转换为字节数组再保存可能效率低下。此时可采用流式处理分块保存文件

以下示例分块保存文件到数据库:

boolean insertFileAsStream(
    int id, 
    String name, 
    int capacity, 
    String filePath
) throws SQLException, IOException {
    String insertSql = """
        INSERT INTO warehouses(id,name,capacity,picture) VALUES(?,?,?,?)
        """;
    try (Connection conn = connect()) {
        if (conn != null) {
            PreparedStatement stmt = conn.prepareStatement(insertSql);
            stmt.setInt(1, id);
            stmt.setString(2, name);
            stmt.setDouble(3, capacity);
            File file = new File(filePath);
            try (FileInputStream fis = new FileInputStream(file)) {
                stmt.setBinaryStream(4, fis, file.length());
                stmt.executeUpdate();
                return true;
            }
        }
    }
    return false;
}

这里不转换图片文件为字节数组,而是通过FileInputStream对象直接流式传输文件内容到数据库,避免全文件加载到内存。这对大文件更高效,可减少OutOfMemoryErrors风险。

⚠️ 踩坑提示:使用流式传输时,确保在try-with-resources中关闭文件流,避免资源泄漏。

4. 从数据库检索BLOB

接下来学习如何从数据库读取二进制数据作为输入流,并直接写入文件输出流

以下方法从数据库检索含BLOB数据的记录:

static boolean writeBlobToFile(
    String query, 
    int paramIndex, 
    int id, 
    String filePath
) throws IOException, SQLException {
    try (
        Connection connection = connect(); 
        PreparedStatement statement = connection.prepareStatement(query)
    ) {
        statement.setInt(paramIndex, id);
        try (
            ResultSet resultSet = statement.executeQuery(); 
            FileOutputStream fileOutputStream = new FileOutputStream(new File(filePath))
        ) {
            while (resultSet.next()) {
                InputStream input = resultSet.getBinaryStream("picture");
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = input.read(buffer)) > 0) {
                    fileOutputStream.write(buffer, 0, bytesRead);
                }
            }
            return true;
        }
    }
}

此代码从数据库检索BLOB并写入文件。创建PreparedStatement对象执行查询,从ResultSet获取二进制流,每次读取1024字节块。注意读取操作返回实际读取字节数,可能小于缓冲区大小。

关键点:使用分块读写处理大文件,避免内存溢出。

5. 总结

本文介绍了如何将二进制文件以字节数组形式写入数据库,以及如何使用流处理大文件。还学习了从数据库检索BLOB文件并写入本地文件的方法。

完整示例代码可在GitHub获取。


原始标题:Store File or byte[] as SQL Blob in Java (Store and Load) | Baeldung