技术开发 频道

Spring 让 LOB 数据操作变得简单易行



三、在 Spring JDBC 中操作 LOB 数据

1.  插入 LOB 数据

    假设我们有一个用于保存论坛帖子的 t_post 表,拥有两个 LOB 字段,其中 post_text 是 CLOB 类型,而 post_attach 是 BLOB 类型。下面,我们来编写插入一个帖子记录的代码:


清单 3. 添加 LOB 字段数据
package com.baobaotao.dao.jdbc;
            …
            import java.sql.PreparedStatement;
            import java.sql.SQLException;
            import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback;
            import org.springframework.jdbc.support.lob.LobCreator;
            import org.springframework.jdbc.support.lob.LobHandler;
            public class PostJdbcDao extends JdbcDaoSupport implements PostDao {
            private LobHandler lobHandler; ① 定义 LobHandler 属性
            public LobHandler getLobHandler() {
            return lobHandler;
            }
            public void setLobHandler(LobHandler lobHandler) {
            this.lobHandler = lobHandler;
            }
            public void addPost(final Post post) {
            String sql = " INSERT INTO t_post(post_id,user_id,post_text,post_attach)"
            + " VALUES(?,?,?,?)";
            getJdbcTemplate().execute(sql,
            new AbstractLobCreatingPreparedStatementCallback(this.lobHandler) { ②
            protected void setValues(PreparedStatement ps,LobCreator lobCreator)
            throws SQLException {
            ps.setInt(1, 1);
            ps.setInt(2, post.getUserId());
            ③ 设置 CLOB 字段
            lobCreator.setClobAsString(ps, 3, post.getPostText());
            ④ 设置 BLOB 字段
            lobCreator.setBlobAsBytes(ps, 4, post.getPostAttach());
            }
            });
            }
            …
            }
            

    首先,我们在 PostJdbcDao 中引入了一个 LobHandler 属性,如 ① 所示,并通过 JdbcTemplate#execute(String sql,AbstractLobCreatingPreparedStatementCallback lcpsc) 方法完成插入 LOB 数据的操作。我们通过匿名内部类的方式定义 LobCreatingPreparedStatementCallback 抽象类的子类,其构造函数需要一个 LobHandler 入参,如 ② 所示。在匿名类中实现了父类的抽象方法 setValues(PreparedStatement ps,LobCreator lobCreator),在该方法中通过 lobCreator 操作 LOB 对象,如 ③、④ 所示,我们分别通过字符串和二进制数组填充 BLOB 和 CLOB 的数据。您同样可以使用流的方式填充 LOB 数据,仅需要调用 lobCreator 相应的流填充方法即可。

    我们需要调整 Spring 的配置文件以配合我们刚刚定义的 PostJdbcDao。假设底层数据库是 Oracle,可以采用以下的配置方式:


清单 4. Oracle 数据库的 LobHandler 配置
…
            <bean id="nativeJdbcExtractor"
            class="org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor"
            lazy-init="true"/>
            <bean id="oracleLobHandler"
            class="org.springframework.jdbc.support.lob.OracleLobHandler"
            lazy-init="true">
            <property name="nativeJdbcExtractor" ref="nativeJdbcExtractor"/> ① 设置本地 Jdbc 对象抽取器
            </bean>
            <bean id="postDao" class="com.baobaotao.dao.jdbc.PostJdbcDao">
            <property name="lobHandler" ref="oracleLobHandler"/> ② 设置 LOB 处理器
            </bean>
            

    大家可能已经注意到 nativeJdbcExtractororacleLobHandler Bean 都设置为 lazy-init="true",这是因为 nativeJdbcExtractor 需要通过运行期的反射机制获取底层的 JDBC 对象,所以需要避免在 Spring 容器启动时就实例化这两个 Bean。

    LobHandler 需要访问本地 JDBC 对象,这一任务委托给 NativeJdbcExtractor Bean 来完成,因此我们在 ① 处为 LobHandler 注入了一个 nativeJdbcExtractor。最后,我们把 lobHandler Bean 注入到需要进行 LOB 数据访问操作的 PostJdbcDao 中,如 ② 所示。

    如果底层数据库是 DB2、SQL Server、MySQL 等非 Oracle 的其它数据库,则只要简单配置一个 DefaultLobHandler 就可以了,如下所示:


清单 5. 一般数据库 LobHandler 的配置
<bean id="defaultLobHandler"
            class="org.springframework.jdbc.support.lob.DefaultLobHandler"
            lazy-init="true"/>
            <bean id="postDao" class="com.baobaotao.dao.jdbc.PostJdbcDao">
            <property name="lobHandler" ref=" defaultLobHandler"/>
            <property name="jdbcTemplate" ref="jdbcTemplate" />
            </bean>
            

   DefaultLobHandler 只是简单地代理标准 JDBC 的 PreparedStatement 和 ResultSet 对象,由于并不需要访问数据库驱动本地的 JDBC 对象,所以它不需要 NativeJdbcExtractor 的帮助。您可以通过以下的代码测试 PostJdbcDao 的 addPost() 方法:


清单 6. 测试 PostJdbcDao 的 addPost() 方法
package com.baobaotao.dao.jdbc;
            import org.springframework.core.io.ClassPathResource;
            import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
            import org.springframework.util.FileCopyUtils;
            import com.baobaotao.dao.PostDao;
            import com.baobaotao.domain.Post;
            public class TestPostJdbcDaoextends AbstractDependencyInjectionSpringContextTests {
            private PostDao postDao;
            public void setPostDao(PostDao postDao) {
            this.postDao = postDao;
            }
            protected String[] getConfigLocations() {
            return new String[]{"classpath:applicationContext.xml"};
            }
            public void testAddPost() throws Throwable{
            Post post = new Post();
            post.setPostId(1);
            post.setUserId(2);
            ClassPathResource res = new ClassPathResource("temp.jpg"); ① 获取图片资源
            byte[] mockImg = FileCopyUtils.copyToByteArray(res.getFile());  ② 读取图片文件的数据
            post.setPostAttach(mockImg);
            post.setPostText("测试帖子的内容");
            postDao.addPost(post);
            }
            }
            

    这里,有几个知识点需要稍微解释一下:AbstractDependencyInjectionSpringContextTests 是 Spring 专门为测试提供的类,它能够直接从 IoC 容器中装载 Bean。此外,我们使用了 ClassPathResource 加载图片资源,并通过 FileCopyUtils 读取文件的数据。ClassPathResourceFileCopyUtils 都是 Spring 提供的非常实用的工具类。

以块数据方式读取 LOB 数据

    您可以直接用数据块的方式读取 LOB 数据:用 String 读取 CLOB 字段的数据,用 byte[] 读取 BLOB 字段的数据。在 PostJdbcDao 中添加一个 getAttachs() 方法,以便获取某一用户的所有带附件的帖子:


清单 7. 以块数据访问 LOB 数据

public List getAttachs(final int userId){
            String sql = "SELECT post_id,post_attach FROM t_post “+
            “where user_id =? and post_attach is not null ";
            return getJdbcTemplate().query(
            sql,new Object[] {userId},
            new RowMapper() {
            public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
            int postId = rs.getInt(1);
            ① 以二进制数组方式获取 BLOB 数据。
            byte[] attach = lobHandler.getBlobAsBytes(rs, 2);
            Post post = new Post();
            post.setPostId(postId);
            post.setPostAttach(attach);
            return post;
            }
            });
            }
            

    通过 JdbcTemplate 的 List query(String sql, Object[] args, RowMapper rowMapper) 接口处理行数据的映射。在 RowMapper 回调的 mapRow() 接口方法中,通过 LobHandler 以 byte[] 获取 BLOB 字段的数据。

以流数据方式读取 LOB 数据

    由于 LOB 数据可能很大(如 100M),如果直接以块的方式操作 LOB 数据,需要消耗大量的内存资源,对应用程序整体性能产生巨大的冲击。对于体积很大的 LOB 数据,我们可以使用流的方式进行访问,减少内存的占用。JdbcTemplate 为此提供了一个 Object query(String sql, Object[] args, ResultSetExtractor rse) 方法,ResultSetExtractor 接口拥有一个处理流数据的抽象类 org.springframework.jdbc.core.support.AbstractLobStreamingResultSetExtractor,可以通过扩展此类用流的方式操作 LOB 字段的数据。下面我们为 PostJdbcDao 添加一个以流的方式获取某个帖子附件的方法:


清单 8. 以流方式访问 LOB 数据

…
            public void getAttach(final int postId,final OutputStream os){ ① 用于接收 LOB 数据的输出流
            String sql = "SELECT post_attach FROM t_post WHERE post_id=? ";
            getJdbcTemplate().query(
            sql, new Object[] {postId},
            new AbstractLobStreamingResultSetExtractor() { ② 匿名内部类
            ③ 处理未找到数据行的情况
            protected void handleNoRowFound() throws LobRetrievalFailureException {
            System.out.println("Not Found result!");
            }
            ④ 以流的方式处理 LOB 字段
            public void streamData(ResultSet rs) throws SQLException, IOException {
            InputStream is = lobHandler.getBlobAsBinaryStream(rs, 1);
            if (is != null) {
            FileCopyUtils.copy(is, os);
            }
            }
            }
            );
            }
            

    通过扩展 AbstractLobStreamingResultSetExtractor 抽象类,在 streamData(ResultSet rs) 方法中以流的方式读取 LOB 字段数据,如 ④ 所示。这里我们又利用到了 Spring 的工具类 FileCopyUtils 将输入流的数据拷贝到输出流中。在 getAttach() 方法中通过入参 OutputStream os 接收 LOB 的数据,如 ① 所示。您可以同时覆盖抽象类中的 handleNoRowFound() 方法,定义未找到数据行时的处理逻辑。

0
相关文章