Spring JDBC设计原理及二次开发使用Spring进行基本的JDBC访问数据库有多种选择。Spring至少提供了三种不同的工作模式:JdbcTemplate, 一个在Spring2.5中新提供的 SimpleJdbc 类能够更好的处理数据库元数据; 还有一种称之为 RDBMS Object 的风格的面向对象封装方式, 有点类似于 JDO 的查询设计。我们在这里简要列举你采取某一种工作方式的主要理由. 不过请注意, 即使你选择了其中的一种工作模式, 你依然可以在你的代码中混用其他任何一种模式以获取其带来的好处和优势。所有的工作模式都必须要求JDBC 2.0以上的数据库驱动的支持, 其中一些高级的功能可能需要 JDBC 3.0 以上的数据库驱动支持。
JdbcTemplate - 这是经典的也是最常用的 Spring 对于 JDBC 访问的方案。这也是最低级别的封装, 其他的工作模式事实上在底层使用了 JdbcTemplate 作为其底层的实现基础。JdbcTemplate 在 JDK 1.4 以上的环境上工作得很好。
NamedParameterJdbcTemplate - 对 JdbcTemplate 做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的 JDBC所使用的“?”作为参数的占位符。这种方式在你需要为某个 SQL 指定许多个参数时,显得更加直观而易用。该特性必须工作在 JDK 1.4 以上。
SimpleJdbcTemplate - 这个类结合了JdbcTemplate和NamedParameterJdbcTemplate的最常用的功能,同时它也利用了一些Java5 的特性所带来的优势,例如泛型、varargs 和 autoboxing 等,从而提供了更加简便的 API 访问方式。需要工作在 Java 5 以上 的环境中。
SimpleJdbcInsert 和 SimpleJdbcCall - 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表名或者存储过程的名称以及一个 Map 作为参数。其中 Map 的 key 需要与数据库表中的字段保持一致。这两个类通常和 SimpleJdbcTemplate 配合使用。这两个类需要工作在 JDK 5 以上,同时数据库需要提供足够的元数据信 息。
RDBMS 对象包括 MappingSqlQuery, SqlUpdate and StoredProcedure - 这种方式允许你在初始化你的数据访问层时创建可重用并且线程安全的对象。该对象在你定义了你的查询语句,声明查询参数并编译相应的 Query 之后被模型化。一旦模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在 JDK 1.4 以上。
异常处理异常结构如下: SQLExceptionTranslator 是一个接口,如果你需要在 SQLException 和org.springframework.dao.DataAccessException 之间作转换,那么必须实现该接口。 转换器类的实现可以采用一般通用的做法(比如使用 JDBC 的 SQLState code),如果为了使转换更准确,也可以进行定制(比如使用 Oracle 的 error code)。
SQLErrorCodeSQLExceptionTranslator 是 SQLExceptionTranslator 的默认实现。 该实现使用指定数据库厂商的 error code,比采用 SQLState 更精确。转换过程基于一个 JavaBean(类型为 SQLErrorCodes)中的 error code。 这个 JavaBean 由SQLErrorCodesFactory 工厂类创建,其中的内容来自于 “sql-error-codes.xml”配置文件。该文件中的数据库厂商代码基于Database MetaData 信息中的 DatabaseProductName,从而配合当前数据库的使用。
SQLErrorCodeSQLExceptionTranslator 使用以下的匹配规则:
首先检查是否存在完成定制转换的子类实现。通常 SQLErrorCodeSQLExceptionTranslator 这个类可以作为一个具体类使用,不需要进行定制,那么这个规则将不适用。
接着将SQLException的error code与错误代码集中的error code进行匹配。默认情况下错误代码集将从SQLErrorCodesFactory取得。 错误代码集来自 classpath 下的 sql-error-codes.xml 文件,它们将与数据库 metadata 信息中的 database name 进行映射。使用 fallback 翻译器。SQLStateSQLExceptionTranslator 类是缺省的 fallback 翻译器。
config模块NamespaceHandler接口,DefaultBeanDefinitionDocumentReader使用该接口来处理在spring xml配置文件中自定义的命名空间。
在 jdbc 模块,我们使用 JdbcNamespaceHandler 来处理 jdbc 配置的命名空间,其代码如下:
1 2 3 4 5 6 7 public class JdbcNamespaceHandler extends NamespaceHandlerSupport { @Override public void init () { registerBeanDefinitionParser("embedded-database" , new EmbeddedDatabaseBeanDefinitionParser()); registerBeanDefinitionParser("initialize-database" , new InitializeDatabaseBeanDefinitionParser()); } }
其中,EmbeddedDatabaseBeanDefinitionParser 继承了 AbstractBeanDefinitionParser,解析元素,并使用EmbeddedDatabaseFactoryBean 创建一个 BeanDefinition。顺便介绍一下用到的软件包 org.w3c.dom。
软件包 org.w3c.dom:为文档对象模型 (DOM) 提供接口,该模型是 Java API for XML Processing 的组件 API。该 Document Object Model Level 2 Core API 允许程序动态访问和更新文档的内容和结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Attr:Attr 接口表示 Element 对象中的属性。 CDATASection: CDATA 节用于转义文本块,该文本块包含的字符如果不转义则会被视为标记。 CharacterData: CharacterData 接口使用属性集合和用于访问 DOM 中字符数据的方法扩展节点。 Comment: 此接口继承自 CharacterData 表示注释的内容,即起始 '<!--' 和结束 '-->' 之间的所有字符。 Document: Document 接口表示整个 HTML 或 XML 文档。 DocumentFragment: DocumentFragment 是“轻量级”或“最小”Document 对象。 DocumentType: 每个 Document 都有 doctype 属性,该属性的值可以为 null,也可以为 DocumentType 对象。 DOMConfiguration: 该 DOMConfiguration 接口表示文档的配置,并维护一个可识别的参数表。 DOMError: DOMError 是一个描述错误的接口。 DOMErrorHandler: DOMErrorHandler 是在报告处理 XML 数据时发生的错误或在进行某些其他处理(如验证文档)时DOM 实现可以调用的回调接口。 DOMImplementation: DOMImplementation 接口为执行独立于文档对象模型的任何特定实例的操作提供了许多方法。 DOMImplementationList: DOMImplementationList 接口提供对 DOM 实现的有序集合的抽象,没有定义或约束如何实现此集合。 DOMImplementationSource: 此接口允许 DOM 实现程序根据请求的功能和版本提供一个或多个实现,如下所述。 DOMLocator: DOMLocator 是一个描述位置(如发生错误的位置)的接口。 DOMStringList: DOMStringList 接口提供对 DOMString 值的有序集合的抽象,没有定义或约束此集合是如何实现的。 Element: Element 接口表示 HTML 或 XML 文档中的一个元素。 Entity: 此接口表示在 XML 文档中解析和未解析的已知实体。 EntityReference: EntityReference 节点可以用来在树中表示实体引用。 NamedNodeMap: 实现 NamedNodeMap 接口的对象用于表示可以通过名称访问的节点的集合。 NameList NameList 接口提供对并行的名称和名称空间值对(可以为 null 值)的有序集合的抽象,无需定义或约束如何实现此集合。 Node: 该 Node 接口是整个文档对象模型的主要数据类型。 NodeList: NodeList 接口提供对节点的有序集合的抽象,没有定义或约束如何实现此集合。 Notation: 此接口表示在 DTD 中声明的表示法。 ProcessingInstruction: ProcessingInstruction 接口表示“处理指令”,该指令作为一种在文档的文本中保持特定于处理器的信息的方法在 XML 中使用。 Text: 该 Text 接口继承自 CharacterData,并且表示 Element 或 Attr 的文本内容(在 XML 中称为 字符数据)。 TypeInfo: TypeInfo 接口表示从 Element 或 Attr 节点引用的类型,用与文档相关的模式指定。 UserDataHandler: 当使用 Node.setUserData() 将一个对象与节点上的键相关联时,当克隆、导入或重命名该对象关联的节点时应用程序可以提供调用的处理程序。
core 模块NativeJdbcExtractor NativeJdbcExtractor 从线程池中的封装的对象中提取出本地的 jdbc 对象,其结构如下: 其实现原理如下(以 c3po 为例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override protected Connection doGetNativeConnection (Connection con) throws SQLException { if (con instanceof C3P0ProxyConnection) { C3P0ProxyConnection cpCon = (C3P0ProxyConnection) con; try { return (Connection) cpCon.rawConnectionOperation( this .getRawConnectionMethod, null , new Object[] {C3P0ProxyConnection.RAW_CONNECTION}); } catch (SQLException ex) { throw ex; } catch (Exception ex) { ReflectionUtils.handleReflectionException(ex); } } return con; }
上述代码通过调用 C3P0 的 rawConnectionOperation api 来获取 Connection,使用 getRawConnection 的回调方法来获取原生的Connection(C3P0 不直接支持原生的 Connection)。NativeJdbcExtractorAdapter 是 NativeJdbcExtractor 的一个简单实现,它的getNativeConnection()方法检查 ConnectionProxy 链,并且代理 doGetNativeConnection 方法。Spring 的TransactionAwareDataSourceProxy 和 LazyConnectionDataSourceProxy 使用 ConnectionProxy。目标 Connection 置于本地连接池中,被子类实现的 doGetNativeConnection 的方法去掉封装获取到原生的 Connection。其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public Connection getNativeConnection (Connection con) throws SQLException { if (con == null ) { return null ; } Connection targetCon = DataSourceUtils.getTargetConnection(con); Connection nativeCon = doGetNativeConnection(targetCon); if (nativeCon == targetCon) { DatabaseMetaData metaData = targetCon.getMetaData(); if (metaData != null ) { Connection metaCon = metaData.getConnection(); if (metaCon != null && metaCon != targetCon) { nativeCon = doGetNativeConnection(metaCon); } } } return nativeCon; }
RowMapper
元数据 metaData 模块本节中 spring 应用到工厂模式,结合代码可以更具体了解。 CallMetaDataProviderFactory 创建 CallMetaDataProvider 的工厂类,其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public static final List<String> supportedDatabaseProductsForProcedures = Arrays.asList( "Apache Derby" , "DB2" , "MySQL" , "Microsoft SQL Server" , "Oracle" , "PostgreSQL" , "Sybase" ); public static final List<String> supportedDatabaseProductsForFunctions = Arrays.asList( "MySQL" , "Microsoft SQL Server" , "Oracle" , "PostgreSQL" ); static public CallMetaDataProvider createMetaDataProvider (DataSource dataSource, final CallMetaDataContext context) { try { return (CallMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, new DatabaseMetaDataCallback() { @Override public Object processMetaData (DatabaseMetaData databaseMetaData) throws SQLException, MetaDataAccessException { String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData(); if (context.isFunction()) { if (!supportedDatabaseProductsForFunctions.contains(databaseProductName)) { if (logger.isWarnEnabled()) { logger.warn(databaseProductName + " is not one of the databases fully supported for function calls " + "-- supported are: " + supportedDatabaseProductsForFunctions); } if (accessProcedureColumnMetaData) { logger.warn("Metadata processing disabled - you must specify all parameters explicitly" ); accessProcedureColumnMetaData = false ; } } } else { if (!supportedDatabaseProductsForProcedures.contains(databaseProductName)) { if (logger.isWarnEnabled()) { logger.warn(databaseProductName + " is not one of the databases fully supported for procedure calls " + "-- supported are: " + supportedDatabaseProductsForProcedures); } if (accessProcedureColumnMetaData) { logger.warn("Metadata processing disabled - you must specify all parameters explicitly" ); accessProcedureColumnMetaData = false ; } } } CallMetaDataProvider provider; if ("Oracle" .equals(databaseProductName)) { provider = new OracleCallMetaDataProvider(databaseMetaData); } else if ("DB2" .equals(databaseProductName)) { provider = new Db2CallMetaDataProvider((databaseMetaData)); } else if ("Apache Derby" .equals(databaseProductName)) { provider = new DerbyCallMetaDataProvider((databaseMetaData)); } else if ("PostgreSQL" .equals(databaseProductName)) { provider = new PostgresCallMetaDataProvider((databaseMetaData)); } else if ("Sybase" .equals(databaseProductName)) { provider = new SybaseCallMetaDataProvider((databaseMetaData)); } else if ("Microsoft SQL Server" .equals(databaseProductName)) { provider = new SqlServerCallMetaDataProvider((databaseMetaData)); } else { provider = new GenericCallMetaDataProvider(databaseMetaData); } if (logger.isDebugEnabled()) { logger.debug("Using " + provider.getClass().getName()); } provider.initializeWithMetaData(databaseMetaData); if (accessProcedureColumnMetaData) { provider.initializeWithProcedureColumnMetaData( databaseMetaData, context.getCatalogName(), context.getSchemaName(), context.getProcedureName()); } return provider; } }); } catch (MetaDataAccessException ex) { throw new DataAccessResourceFailureException("Error retreiving database metadata" , ex); } }
TableMetaDataProviderFactory 创建 TableMetaDataProvider 工厂类,其创建过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public static TableMetaDataProvider createMetaDataProvider (DataSource dataSource, final TableMetaDataContext context, final NativeJdbcExtractor nativeJdbcExtractor) { try { return (TableMetaDataProvider) JdbcUtils.extractDatabaseMetaData(dataSource, new DatabaseMetaDataCallback() { @Override public Object processMetaData (DatabaseMetaData databaseMetaData) throws SQLException { String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData(); TableMetaDataProvider provider; if ("Oracle" .equals(databaseProductName)) { provider = new OracleTableMetaDataProvider(databaseMetaData, context.isOverrideIncludeSynonymsDefault()); } else if ("HSQL Database Engine" .equals(databaseProductName)) { provider = new HsqlTableMetaDataProvider(databaseMetaData); } else if ("PostgreSQL" .equals(databaseProductName)) { provider = new PostgresTableMetaDataProvider(databaseMetaData); } else if ("Apache Derby" .equals(databaseProductName)) { provider = new DerbyTableMetaDataProvider(databaseMetaData); } else { provider = new GenericTableMetaDataProvider(databaseMetaData); } if (nativeJdbcExtractor != null ) { provider.setNativeJdbcExtractor(nativeJdbcExtractor); } if (logger.isDebugEnabled()) { logger.debug("Using " + provider.getClass().getSimpleName()); } provider.initializeWithMetaData(databaseMetaData); if (accessTableColumnMetaData) { provider.initializeWithTableColumnMetaData(databaseMetaData, context.getCatalogName(), context.getSchemaName(), context.getTableName()); } return provider; } }); } catch (MetaDataAccessException ex) { throw new DataAccessResourceFailureException("Error retrieving database metadata" , ex); } }
使用 SqlParameterSource 提供参数值使用 Map 来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring 提供了一些其他的 SqlParameterSource实现类来指定参数值。 我们首先可以看看 BeanPropertySqlParameterSource 类,这是一个非常简便的指定参数的实现类,只要你有一个符合 JavaBean 规范的类就行了。它将使用其中的 getter 方法来获取参数值。
SqlParameter 封装了定义 sql 参数的对象。CallableStateMentCallback,PrePareStateMentCallback,StateMentCallback,ConnectionCallback 回调类分别对应 JdbcTemplate 中的不同处理方法。
simple 实现
DataSourcespring 通过 DataSource 获取数据库的连接。Datasource 是 jdbc 规范的一部分,它通过 ConnectionFactory 获取。一个容器和框架可以在应用代码层中隐藏连接池和事务管理。
当使用 spring 的 jdbc 层,你可以通过 JNDI 来获取 DataSource,也可以通过你自己配置的第三方连接池实现来获取。流 行的第三方实现由 apache Jakarta Commons dbcp 和 c3p0.
TransactionAwareDataSourceProxy 作为目标 DataSource 的一个代理, 在对目标 DataSource 包装的同时,还增加了 Spring 的事务管理能力, 在这一点上,这个类的功能非常像 J2EE 服务器所提供的事务化的 JNDI DataSource。
Note 该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource 接口实现作为参数。 这种情况下, 这个类可以使现有代码参与 Spring 的事务管理。通常最好的做法是使用更高层的抽象 来对数据源进行管理,比如 JdbcTemplate 和 DataSourceUtils 等等。
注意:DriverManagerDataSource 仅限于测试使用,因为它没有提供池的功能,这会导致在多个请求获取连接时性能很差。
object模块
JdbcTemplate 是 core 包的核心类它替我们完成了资源的创建以及释放工作,从而简化了我们对 JDBC 的使用。 它还可以帮助我们避免一些常见的错误,比 如忘记关闭数据库连接。 JdbcTemplate 将完成 JDBC 核心处理流程,比如 SQL 语句的创建、执行,而把 SQL 语句的生成以 及查询结果的提取工作留给我们的应用代码。 它可以完成 SQL 查询、更新以及调用存储过程,可以对 ResultSet 进行遍历并 加以提取。 它还可以捕获 JDBC 异常并将其转换成 org.springframework.dao 包中定义的,通用的,信息更丰富的异常。
使用 JdbcTemplate 进行编码只需要根据明确定义的一组契约来实现回调接口。 PreparedStatementCreator 回调接口通过给定的 Connection 创建一个 PreparedStatement,包含 SQL 和任何相关的参数。 CallableStatementCreateor 实现同样的处理,只不过它创建的是 CallableStatement。 RowCallbackHandler 接口则从数据集的每一行中提取值。
我们可以在 DAO 实现类中通过传递一个 DataSource 引用来完成 JdbcTemplate 的实例化,也可以在 Spring 的 IoC 容器中配置一个 JdbcTemplate 的 bean 并赋予 DAO 实现类作为一个实例。 需要注意的是 DataSource 在 Spring 的 IoC 容器中总是配制成一个 bean,第一种情况下,DataSource bean 将传递给 service,第二种情况下 DataSource bean 传递给 JdbcTemplate bean。
NamedParameterJdbcTemplateNamedParameterJdbcTemplate 类为 JDBC 操作增加了命名参数的特性支持,而不是传统的使用(‘?’)作为参数的占位符。 NamedParameterJdbcTemplate 类对 JdbcTemplate 类进行了封装, 在底层,JdbcTemplate 完成了多数的工作。