跳转至

CLOB 大对象

本文你会学到

  • CLOB 和 BLOB 的区别及各自适用场景
  • 通过 createClob + setCharacterStream 流式写入大文本
  • 通过 getClob + getSubString 读取 CLOB 数据
  • 大对象资源释放的重要性

📝 文章太长,VARCHAR 存不下怎么办?

当你需要存储文章内容、JSON 文档、XML 或日志等大文本数据时,VARCHAR(255) 显然不够用。这时就需要 CLOB(Character Large Object)——JDBC 中专门处理大文本的类型。与之对应的 BLOB(Binary Large Object)用于存储二进制数据(图片、视频、音频)。

在 MySQL 中,CLOB 对应的类型是 TEXT / LONGTEXT;在 Oracle 中则直接使用 CLOB 类型。JDBC 提供了统一的 API 来操作这些大对象,屏蔽了不同数据库的差异。

✍️ 写入 CLOB 数据

createClob + setCharacterStream 方式

Connection.createClob() 创建一个空的 Clob 对象,再通过 setCharacterStream(1) 获取 Writer 进行流式写入。这种方式适合大文本场景(MB 级别),数据通过流逐步写入,不会一次性占用内存。

createClob() + setCharacterStream() 写入 CLOB
    /**
     * CLOB 写入:使用 Connection.createClob() + setCharacterStream 写入大文本
     */
    @Test
    @DisplayName("CLOB 写入:createClob + setCharacterStream")
    void testClobWrite() throws SQLException {
        String longText = "JDBC(Java Database Connectivity)是 Java 访问数据库的标准 API。"
                + "它提供了一套统一的接口,使 Java 程序能够以数据库无关的方式执行 SQL 语句、"
                + "检索结果集,并管理数据库连接。JDBC API 由 java.sql 和 javax.sql 包组成,"
                + "是 Java SE 和 Java EE 平台的核心组成部分。通过 JDBC,开发者可以连接各种关系型数据库,"
                + "包括 MySQL、PostgreSQL、Oracle、SQL Server 等。";

        // 通过 Connection.createClob() 创建空的 Clob 对象
        Clob clob = conn.createClob();
        // 获取 Writer,从位置 1 开始写入(CLOB 位置从 1 开始)
        try (Writer writer = clob.setCharacterStream(1)) {
            writer.write(longText);
        } catch (IOException e) {
            throw new SQLException("写入 CLOB 内容失败", e);
        }

        // 使用 PreparedStatement.setClob() 绑定并插入
        String sql = "INSERT INTO documents (name, content) VALUES (?, ?)";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, "JDBC 简介");
            ps.setClob(2, clob);
            int affectedRows = ps.executeUpdate();
            assertEquals(1, affectedRows, "应成功插入 1 条记录");
        }

        // 验证插入成功:查询记录数
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM documents")) {
            assertTrue(rs.next(), "应能查询到记录");
            assertEquals(1, rs.getInt(1), "documents 表应有 1 条记录");
        }
    }

setString 直接写入

PreparedStatement.setString() 可以直接将字符串写入 CLOB 列,这是最简单的方式。但数据量较大时(如超过 JVM 堆内存),会将整个文本加载到内存中,不适合超大文本。

setString() 直接写入 CLOB
    /**
     * CLOB 写入:直接使用 setString() 插入文本(适合较小内容)
     */
    @Test
    @DisplayName("CLOB 写入:setString 直接方式")
    void testClobWriteString() throws SQLException {
        String content = "这是一段通过 setString() 直接写入的文本。"
                + "对于较小的文本内容,无需创建 Clob 对象,直接使用 setString() 即可。"
                + "H2 数据库的 CLOB 类型支持直接设置字符串值,简化了操作流程。";

        String sql = "INSERT INTO documents (name, content) VALUES (?, ?)";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, "简短文档");
            // 直接用 setString() 设置 CLOB 列值(H2 支持,Oracle 等对大文本需用 setClob)
            ps.setString(2, content);
            int affectedRows = ps.executeUpdate();
            assertEquals(1, affectedRows, "应成功插入 1 条记录");
        }

        // 验证插入内容
        try (PreparedStatement ps = conn.prepareStatement(
                "SELECT content FROM documents WHERE name = ?")) {
            ps.setString(1, "简短文档");
            try (ResultSet rs = ps.executeQuery()) {
                assertTrue(rs.next(), "应能查询到刚插入的记录");
                assertEquals(content, rs.getString("content"), "读取的内容应与写入的一致");
            }
        }
    }

📖 读取 CLOB 数据

读取 CLOB 数据有两种常见方式:

  • ResultSet.getClob() 返回 Clob 对象,通过 getSubString() 提取全部或部分内容,适合需要分段读取的场景
  • ResultSet.getString() 直接返回完整字符串,JDBC 驱动会自动处理 CLOB 到 String 的转换
getClob() + getSubString() 读取 CLOB
    /**
     * CLOB 读取:使用 getClob() 和 getString() 两种方式读取大文本
     */
    @Test
    @DisplayName("CLOB 读取:getClob 和 getString")
    void testClobRead() throws SQLException {
        // 先插入一条测试数据
        String expectedContent = "CLOB(Character Large Object)用于存储大文本数据,"
                + "最大可存储数 GB 的字符数据。与 VARCHAR 不同,CLOB 专门为海量文本设计,"
                + "适合存储文章内容、日志文件、XML/JSON 文档等大文本场景。"
                + "在 JDBC 中,通过 java.sql.Clob 接口操作 CLOB 数据,"
                + "支持流式读写以避免将全部内容加载到内存。";
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO documents (name, content) VALUES (?, ?)")) {
            ps.setString(1, "CLOB 说明文档");
            ps.setString(2, expectedContent);
            ps.executeUpdate();
        }

        // 方式一:使用 ResultSet.getClob() 获取 Clob 对象,再通过 getSubString() 读取
        try (PreparedStatement ps = conn.prepareStatement(
                "SELECT name, content FROM documents WHERE name = ?")) {
            ps.setString(1, "CLOB 说明文档");
            try (ResultSet rs = ps.executeQuery()) {
                assertTrue(rs.next(), "应能查询到记录");

                // getClob() 返回 Clob 对象
                Clob clob = rs.getClob("content");
                assertNotNull(clob, "Clob 对象不应为 null");

                // getSubString(pos, length): 从指定位置读取指定长度的子串(位置从 1 开始)
                String contentFromClob = clob.getSubString(1, (int) clob.length());
                assertEquals(expectedContent, contentFromClob,
                        "通过 Clob.getSubString() 读取的内容应与写入的一致");

                // 方式二:直接使用 getString(),更简洁(适合较小的 CLOB)
                String contentFromString = rs.getString("content");
                assertEquals(expectedContent, contentFromString,
                        "通过 getString() 读取的内容应与写入的一致");
            }
        }
    }

推荐使用 getString()

对于大多数场景,直接使用 ResultSet.getString() 读取 CLOB 列是最简单的方式。JDBC 驱动会自动将 CLOB 内容转换为 String。只有处理超大文本(如 100MB+)时才需要使用 getCharacterStream() 流式读取。

⚖️ BLOB vs CLOB 对比

维度 BLOB CLOB
全称 Binary Large Object Character Large Object
存储内容 二进制数据(图片、视频、音频) 字符数据(文章、JSON、XML)
JDBC 类型 Blob Clob
创建方法 Connection.createBlob() Connection.createClob()
写入方式 setBinaryStream() setCharacterStream()
读取方式 getBinaryStream() getCharacterStream() / getString()
简单写入 setBytes() setString()
MySQL 类型 BLOB / LONGBLOB TEXT / LONGTEXT

选择原则很简单:存储的是字符文本就用 CLOB,存储的是二进制数据就用 BLOB

♻️ 资源释放

ClobBlob 对象在创建它们的事务期间一直有效,占用数据库服务端的资源。如果长事务中持有大量大对象而不释放,可能导致资源耗尽。使用完毕后应调用 free() 主动释放。

free() 释放 CLOB 资源
    /**
     * CLOB 资源释放:演示 free() 方法的使用
     * <p>
     * 对于大型 CLOB 对象,在长事务中及时调用 free() 可以释放数据库端和驱动端的资源。
     * H2 的内存数据库中 free() 效果不明显,但在 Oracle 等真实数据库中,
     * 未释放的 CLOB 可能占用临时表空间,尤其在批量处理场景下。
     */
    @Test
    @DisplayName("CLOB 资源释放:free() 方法")
    void testClobFree() throws SQLException {
        // 通过 Connection.createClob() 创建 Clob 对象
        Clob clob = conn.createClob();
        assertNotNull(clob, "createClob() 应返回非 null 的 Clob 对象");

        String expectedText = "用于演示 free() 的测试文本,在实际生产环境中,"
                + "处理完大文本后应及时释放资源。";
        try (Writer writer = clob.setCharacterStream(1)) {
            writer.write(expectedText);
        } catch (IOException e) {
            throw new SQLException("写入 CLOB 内容失败", e);
        }

        // 验证 Clob 对象可用
        assertEquals(expectedText.length(), clob.length(), "Clob 长度应与写入的文本一致");

        // 调用 free() 释放资源
        clob.free();

        // 释放后再次访问 Clob 会抛出 SQLException
        assertThrows(SQLException.class, () -> clob.length(),
                "free() 之后访问 Clob 应抛出 SQLException");
    }

及时释放大对象资源

ClobBlob 对象在创建它们的事务期间一直有效。长事务可能导致资源耗尽,应在使用完毕后调用 free() 主动释放。对于小文本使用 setString() / getString() 方式则无需手动管理。