本文讲解了在 Spring 中处理 LOB 数据的原理和方法,对于 Spring JDBC 以及 Spring 所集成的第三方 ORM 框架(包括 JPA、Hibernate 和 iBatis)如何处理 LOB 数据进行了阐述。
概述
LOB 代表大对象数据,包括 BLOB 和 CLOB 两种类型,前者用于存储大块的二进制数据,如图片数据,视频数据等,而后者用于存储长文本数据,如论坛的帖子内容,产品的详细描述等。值得注意的是:在不同的数据库中,大对象对应的字段类型是不尽相同的,如 DB2 对应 BLOB/CLOB,MySql 对应 BLOB/LONGTEXT,SqlServer 对应 IMAGE/TEXT。需要指出的是,有些数据库的大对象类型可以象简单类型一样访问,如 MySql 的 LONGTEXT 的操作方式和 VARCHAR 类型一样。在一般情况下, LOB 类型数据的访问方式不同于其它简单类型的数据,我们经常会以流的方式操作 LOB 类型的数据。此外,LOB 类型数据的访问不是线程安全的,需要为其单独分配相应的数据库资源,并在操作完成后释放资源。最后,Oracle 9i 非常有个性地采用非 JDBC 标准的 API 操作 LOB 数据。所有这些情况给编写操作 LOB 类型数据的程序带来挑战,Spring 在 org.springframework.jdbc.support.lob 包中为我们提供了相应的帮助类,以便我们轻松应对这头拦路虎。 Spring 大大降低了我们处理 LOB 数据的难度。首先,Spring 提供了 NativeJdbcExtractor 接口,您可以在不同环境里选择相应的实现类从数据源中获取本地 JDBC 对象;其次,Spring 通过 LobCreator 接口取消了不同数据厂商操作 LOB 数据的差别,并提供了创建 LobCreator 的 LobHandler 接口,您只要根据底层数据库类型选择合适的 LobHandler 进行配置即可。 本文将详细地讲述通过 Spring JDBC 插入和访问 LOB 数据的具体过程。不管是以块的方式还是以流的方式,您都可以通过 LobCreator 和 LobHandler 方便地访问 LOB 数据。对于 ORM 框架来说,JPA 拥有自身处理 LOB 数据的配置类型,Spring 为 Hibernate 和 iBatis 分别提供了 LOB 数据类型的配置类,您仅需要使用这些类进行简单的配置就可以像普通类型一样操作 LOB 类型数据。 本地 JDBC 对象 当您在 Web 应用服务器或 Spring 中配置数据源时,从数据源中返回的数据连接对象是本地 JDBC 对象(如 DB2Connection、OracleConnection)的代理类,这是因为数据源需要改变数据连接一些原有的行为以便对其进行控制:如调用 Connection#close() 方法时,将数据连接返回到连接池中而非将其真的关闭。 在访问 LOB 数据时,根据数据库厂商的不同,可能需要使用被代理前的本地 JDBC 对象(如 DB2Connection 或 DB2ResultSet)特有的 API。为了从数据源中获取本地 JDBC 对象, Spring 定义了 org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor 接口并提供了相应的实现类。NativeJdbcExtractor 定义了从数据源中抽取本地 JDBC 对象的若干方法:
有些简单的数据源仅对 Connection 对象进行代理,这时可以直接使用 SimpleNativeJdbcExtractor 实现类。但有些数据源(如 Jakarta Commons DBCP)会对所有的 JDBC 对象进行代理,这时,就需要根据具体的情况选择适合的抽取器实现类了。下表列出了不同数据源本地 JDBC 对象抽取器的实现类:
下面的代码演示了从 DBCP 数据源中获取 DB2 的本地数据库连接 DB2Connection 的方法: 清单 1. 获取本地数据库连接
在 ① 处我们通过 DataSourceUtils 获取当前线程绑定的数据连接,为了使用线程上下文相关的事务,通过 DataSourceUtils 从数据源中获取连接是正确的做法,如果直接通过 dateSource 获取连接,则将得到一个和当前线程上下文无关的数据连接实例。 JdbcTemplate 可以在配置时注入一个本地 JDBC 对象抽取器,要使代码 清单 1 正确运行,我们必须进行如下配置: 清单 2. 为 JdbcTemplate 装配本地 JDBC 对象抽取器
在获取 DB2 的本地 Connection 实例后,我们就可以使用该对象的一些特有功能了,如使用 DB2Connection 的特殊 API 对 LOB 对象进行操作。
LobCreator
虽然 JDBC 定义了两个操作 LOB 类型的接口:java.sql.Blob 和 java.sql.Clob,但有些厂商的 JDBC 驱动程序并不支持这两个接口。为此,Spring 定义了一个独立于 java.sql.Blob/Clob 的 LobCreator 接口,以统一的方式操作各种数据库的 LOB 类型数据。因为 LobCreator 本身持有 LOB 所对应的数据库资源,所以它不是线程安全的,一个 LobCreator 只能操作一个 LOB 数据。 为了方便在 PreparedStatement 中使用 LobCreator,您可以直接使用 JdbcTemplate#execute(String sql,AbstractLobCreatingPreparedStatementCallback lcpsc) 方法。下面对 LobCreator 接口中的方法进行简要说明:
LobHandler LobHandler 接口为操作 BLOB/CLOB 提供了统一访问接口,而不管底层数据库究竟是以大对象的方式还是以一般数据类型的方式进行操作。此外,LobHandler 还充当了 LobCreator 的工厂类。 大部分数据库厂商的 JDBC 驱动程序(如 DB2)都以 JDBC 标准的 API 操作 LOB 数据,但 Oracle 9i 及以前的 JDBC 驱动程序采用了自己的 API 操作 LOB 数据,Oracle 9i 直接使用自己的 API 操作 LOB 数据,且不允许通过 PreparedStatement 的 setAsciiStream()、setBinaryStream()、setCharacterStream() 等方法填充流数据。Spring 提供 LobHandler 接口主要是为了迁就 Oracle 特立独行的作风。所以 Oracle 必须使用 OracleLobHandler 实现类,而其它的数据库统一使用 DefaultLobHandler 就可以了。Oracle 10g 改正了 Oracle 9i 这个异化的风格,终于天下归一了,所以 Oracle 10g 也可以使用 DefaultLobHandler。 下面,我们来看一下 LobHandler 接口的几个重要方法:
在 Spring JDBC 中操作 LOB 数据
插入 LOB 数据 假设我们有一个用于保存论坛帖子的 t_post 表,拥有两个 LOB 字段,其中 post_text 是 CLOB 类型,而 post_attach 是 BLOB 类型。下面,我们来编写插入一个帖子记录的代码: 清单 3. 添加 LOB 字段数据
首先,我们在 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 配置
大家可能已经注意到 nativeJdbcExtractor 和 oracleLobHandler 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 的配置
DefaultLobHandler 只是简单地代理标准 JDBC 的 PreparedStatement 和 ResultSet 对象,由于并不需要访问数据库驱动本地的 JDBC 对象,所以它不需要 NativeJdbcExtractor 的帮助。您可以通过以下的代码测试 PostJdbcDao 的 addPost() 方法: 清单 6. 测试 PostJdbcDao 的 addPost() 方法
这里,有几个知识点需要稍微解释一下:AbstractDependencyInjectionSpringContextTests 是 Spring 专门为测试提供的类,它能够直接从 IoC 容器中装载 Bean。此外,我们使用了 ClassPathResource 加载图片资源,并通过 FileCopyUtils 读取文件的数据。ClassPathResource 和 FileCopyUtils 都是 Spring 提供的非常实用的工具类。
以块数据方式读取 LOB 数据
您可以直接用数据块的方式读取 LOB 数据:用 String 读取 CLOB 字段的数据,用 byte[] 读取 BLOB 字段的数据。在 PostJdbcDao 中添加一个 getAttachs() 方法,以便获取某一用户的所有带附件的帖子: 清单 7. 以块数据访问 LOB 数据
通过 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 数据
通过扩展 AbstractLobStreamingResultSetExtractor 抽象类,在 streamData(ResultSet rs) 方法中以流的方式读取 LOB 字段数据,如 ④ 所示。这里我们又利用到了 Spring 的工具类 FileCopyUtils 将输入流的数据拷贝到输出流中。在 getAttach() 方法中通过入参 OutputStream os 接收 LOB 的数据,如 ① 所示。您可以同时覆盖抽象类中的 handleNoRowFound() 方法,定义未找到数据行时的处理逻辑。 在 JPA 中操作 LOB 数据 在 JPA 中 LOB 类型的持久化更加简单,仅需要通过特殊的 LOB 注释(Annotation)就可以达到目的。我们对 Post 中的 LOB 属性类型进行注释: 清单 9. 注释 LOB 类型属性
postText 属性对应 T_POST 表的 POST_TEXT 字段,该字段的类型是 LONTTEXT,并且非空。JPA 通过 @Lob 将属性标注为 LOB 类型,如 ①-1 和 ②-1 所示。通过 @Basic 指定 LOB 类型数据的获取策略,FetchType.EAGER 表示非延迟加载,而 FetchType.LAZY 表示延迟加载,如 ①-2 和 ②-2 所示。通过 @Column 的 columnDefinition 属性指定数据表对应的 LOB 字段类型,如 ①-3 和 ②-3 所示。
在 Hibernate 中操作 LOB 数据
下面我们使用 Spring 的 UserType 为 Post 配置 Hibernate 的映射文件,如 清单 10 所示: 清单 10 . LOB 数据映射配置
postText 为 String 类型的属性,对应数据库的 CLOB 类型,而 postAttach 为 byte[] 类型的属性,对应数据库的 BLOB 类型。分别使用 Spring 所提供的相应 UserType 实现类进行配置,如 ① 和 ② 处所示。 在配置好映射文件后,还需要在 Spring 配置文件中定义 LOB 数据处理器,让 SessionFactory 拥有处理 LOB 数据的能力: 清单 11 . 将 LobHandler 注入到 SessionFactory 中
在一般的数据库(如 DB2)中,仅需要简单地使用 HibernateTemplate#save(Object entity) 等方法就可以正确的保存 LOB 数据了。如果是 Oracle 9i 数据库,还需要配置一个本地 JDBC 抽取器,并使用特定的 LobHandler 实现类,如 清单 4 所示。 使用 LobHandler 操作 LOB 数据时,需要在事务环境下才能工作,所以必须事先配置事务管理器,否则会抛出异常。 在 iBatis 中操作 LOB 数据 iBatis 为处理不同类型的数据定义了一个统一的接口:com.ibatis.sqlmap.engine.type.TypeHandler。这个接口类似于 Hibernate 的 UserType。iBatis 本身拥有该接口的众多实现类,如 LongTypeHandler、DateTypeHandler 等,但没有为 LOB 类型提供对应的实现类。Spring 在 org.springframework.orm.ibatis.support 包中为我们提供了几个处理 LOB 类型的 TypeHandler 实现类:
当结果集中包括 LOB 数据时,需要在结果集映射配置项中指定对应的 Handler 类,下面我们采用 Spring 所提供的实现类对 Post 结果集的映射进行配置。 清单 12 . 对 LOB 数据进行映射
当 iBatis 引擎从结果集中读取或更改 LOB 类型数据时,都需要指定处理器。我们在 ① 和 ② 处为读取 LOB 类型的数据指定处理器,相似的,在 ③ 和 ④ 处为插入 LOB 类型的数据也指定处理器。 此外,我们还必须为 SqlClientMap 提供一个 LobHandler: 清单 13. 将 LobHandler 注入到 SqlClientMap 中
处理 LOB 数据时,Spring 要求在事务环境下工作,所以还必须配置一个事务管理器。iBatis 的事务管理器和 Spring JDBC 事务管理器相同,此处不再赘述。 小结 本文就 Spring 中如何操作 LOB 数据进行较为全面的讲解,您仅需简单地配置 LobHandler 就可以直接在程序中象一般数据一样操作 LOB 数据了。对于 ORM 框架来说,Spring 为它们分别提供了支持类,您仅要使用相应的支持类进行配置就可以了。因此您会发现在传统 JDBC 程序操作 LOB 头疼的问题将变得轻松了许多。 |