# 数据访问 引用文档的这一部分涉及数据访问以及数据访问层和业务或服务层之间的交互。 Spring 的全面事务管理支持进行了一些详细的介绍,随后是对 Spring 框架与之集成的各种数据访问框架和技术的全面介绍。 ## 1. 事务管理 全面的事务支持是使用 Spring 框架的最有说服力的理由之一。 Spring 框架为事务管理提供了一个一致的抽象,它提供了以下好处: * 跨不同事务 API 的一致的编程模型,例如 爪哇 事务 API、JDBC、 Hibernate 和 爪哇 持久性 API( JPA)。 * 支持[声明式事务管理](#transaction-declarative)。 * 用于[programmatic](#transaction-programmatic)事务管理的比复杂事务 API(如 JTA)更简单的 API。 * 与 Spring 的数据访问抽象进行了出色的集成。 以下章节描述了 Spring 框架的事务特性和技术: * [Advantages of the Spring Framework’s transaction support model](#transaction-motivation)描述了为什么要使用 Spring 框架的事务抽象,而不是使用 EJB 容器管理事务(CMT),或者选择通过专有 API(如 Hibernate)来驱动本地事务。 * [Understanding the Spring Framework transaction abstraction](#transaction-strategies)概述了核心类,并描述了如何从各种源配置和获取`DataSource`实例。 * [将资源与事务同步](#tx-resource-synchronization)描述了应用程序代码如何确保正确地创建、重用和清理资源。 * [声明式事务管理](#transaction-declarative)描述了对声明式事务管理的支持。 * [程序化事务管理](#transaction-programmatic)涵盖了对程序化(即显式编码)事务管理的支持。 * [事务绑定事件](#transaction-event)描述如何在事务中使用应用程序事件。 本章还讨论了最佳实践,[应用服务器集成](#transaction-application-server-integration)和[常见问题的解决方案](#transaction-solutions-to-common-problems)。 ### 1.1. Spring 框架的事务支持模型的优点 传统上,爪哇 EE 开发人员在事务管理方面有两种选择:全局事务或本地事务,这两种选择都有很大的局限性。在接下来的两节中,将介绍全局和本地事务管理,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。 #### 1.1.1.全球交易 全局事务允许你使用多个事务资源,通常是关系数据库和消息队列。应用程序服务器通过 JTA 管理全局事务,JTA 是一个繁琐的 API(部分原因是其异常模型)。此外,JTA`UserTransaction`通常需要来自 JNDI,这意味着你还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。 以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是声明式事务管理的一种形式(区别于程序化事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需求,尽管 EJB 本身的使用需要使用 JNDI。它消除了编写 爪哇 代码来控制事务的大部分(但不是全部)需求。最大的缺点是,CMT 与 JTA 和应用程序服务器环境绑定在一起。而且,只有当你选择在 EJB 中实现业务逻辑(或者至少在事务性 EJB facade 的后面)时,它才可用。总的来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的令人信服的替代方案时。 #### 1.1.2.本地交易 本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,通过使用 JDBC 连接来管理事务的代码不能在全局 JTA 事务中运行。由于应用程序服务器不参与事务管理,因此它无法帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源。)另一个缺点是,本地事务对编程模型具有侵入性。 #### 1.1.3. Spring 框架的一致性编程模型 Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。你只需编写一次代码,就可以从不同环境中的不同事务管理策略中受益。 Spring 框架提供了声明式和程序化的事务管理。大多数用户更喜欢声明式事务管理,我们在大多数情况下都推荐这种方法。 对于程序化事务管理,开发人员使用 Spring 框架事务抽象,它可以运行在任何底层事务基础设施上。对于优选的声明式模型,开发人员通常很少或根本不编写与事务管理相关的代码,因此,不依赖于 Spring Framework Transaction API 或任何其他事务 API。 你需要用于事务管理的应用程序服务器吗? Spring 框架的事务管理支持改变了 Enterprise爪哇 应用程序何时需要应用服务器的传统规则。 特别是,你不需要通过 EJB 进行完全用于声明式事务的应用程序服务器。实际上,即使你的应用程序服务器具有强大的 JTA 功能,你也可能认为 Spring 框架的声明式事务提供了比 EJB CMT 更强大的功能和更高效的编程模型。 通常,只有当你的应用程序需要跨多个资源处理事务时,你才需要应用程序服务器的 JTA 功能,而这对许多应用程序来说并不是必需的。许多高端应用程序使用单一的、高度可扩展的数据库(如 Oracle RAC)。独立事务管理器(例如[Atomikos 交易](https://www.atomikos.com/)和[JOTM](http://jotm.objectweb.org/))是其他选项。当然,你可能需要其他应用程序服务器功能,例如 爪哇 消息服务和 爪哇 EE 连接器架构。 Spring 框架为你提供了何时将应用程序扩展到全负载应用程序服务器的选择。使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的事务)编写代码,如果你需要在全局的、容器管理的事务中运行这些代码,那么你将面临大量的返工,这种情况已经一去不复返了。在 Spring 框架中,只有配置文件中的 Bean 定义中的一些需要更改(而不是代码)。 ### 1.2.理解 Spring 框架事务抽象 Spring 事务抽象的关键是事务策略的概念。事务策略由`TransactionManager`定义,特别是用于强制事务管理的 `org.springframework.transactionmanager’接口和用于反应式事务管理的 `org.springframework.transactionmanager’接口。下面的列表显示了“Platform TransactionManager”API 的定义: ``` public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; } ``` 这主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它[程式设计](#transaction-programmatic-ptm)。由于“Platform TransactionManager”是一个界面,因此可以在必要时轻松地对其进行模拟或删除。它不绑定到查找策略,例如 JNDI。“Platform TransactionManager”实现的定义与 Spring Framework IoC 容器中的任何其他对象(或 Bean)一样。仅这一点就使 Spring 框架事务成为一个有价值的抽象,即使在使用 JTA 的情况下也是如此。与直接使用 JTA 相比,你可以更容易地测试事务性代码。 同样,与 Spring 的哲学一致,可以由`PlatformTransactionManager`接口的任何方法抛出的`TransactionException`不受检查(也就是说,它扩展了`java.lang.RuntimeException`类)。事务基础设施故障几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理`TransactionException`。最重要的一点是,开发商并不是被迫这么做的。 `getTransaction(..)`方法返回一个`TransactionStatus`对象,这取决于一个 `transactiondefinition’参数。如果当前调用堆栈中存在匹配的事务,则返回的`TransactionStatus`可能表示一个新事务,或者可以表示一个现有事务。后一种情况的含义是,与 爪哇 EE 事务上下文一样,`TransactionStatus`与执行线程相关联。 在 Spring 框架 5.2 中, Spring 还为使用反应性类型或 Kotlin 协程的反应性应用程序提供了事务管理抽象。下面的列表显示了由 `org.springframework.transaction.reactiveTransactionManager’定义的事务策略: ``` public interface ReactiveTransactionManager extends TransactionManager { Mono getReactiveTransaction(TransactionDefinition definition) throws TransactionException; Mono commit(ReactiveTransaction status) throws TransactionException; Mono rollback(ReactiveTransaction status) throws TransactionException; } ``` 反应式事务管理器主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它[程式设计](#transaction-programmatic-rtm)。因为`ReactiveTransactionManager`是一个接口,所以可以根据需要轻松地对其进行模拟或截断。 `TransactionDefinition`接口指定: * 传播:通常,事务范围内的所有代码都在该事务中运行。但是,如果事务方法在事务上下文已经存在的情况下运行,则可以指定该行为。例如,代码可以在现有的事务中继续运行(常见的情况),或者可以暂停现有的事务并创建新的事务。 Spring 提供了从 EJB CMT 中熟悉的所有事务传播选项。要了解 Spring 中事务传播的语义,请参见[事务传播](#tx-propagation)。 * 隔离:此事务与其他事务的工作隔离的程度。例如,这个事务可以看到来自其他事务的未提交的写吗? * 超时:此事务在超时和被底层事务基础设施自动回滚之前运行了多长时间。 * 只读状态:当你的代码读取但不修改数据时,你可以使用只读事务。只读事务在某些情况下可以是一种有用的优化,例如当你使用 Hibernate 时。 这些设置反映了标准的事务概念。如果有必要,请参考讨论事务隔离级别和其他核心事务概念的资源。理解这些概念对于使用 Spring 框架或任何事务管理解决方案是至关重要的。 `TransactionStatus`接口为事务代码控制事务执行和查询事务状态提供了一种简单的方法。这些概念应该是熟悉的,因为它们是所有事务 API 所共有的。下面的列表显示了“TransactionStatus”接口: ``` public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { @Override boolean isNewTransaction(); boolean hasSavepoint(); @Override void setRollbackOnly(); @Override boolean isRollbackOnly(); void flush(); @Override boolean isCompleted(); } ``` 无论你是否 OPT 用于 Spring 中的声明式或程序化事务管理,定义正确的`TransactionManager`实现是绝对必要的。你通常通过依赖注入来定义这个实现。 `TransactionManager`实现通常需要它们工作的环境的知识:JDBC、JTA、 Hibernate 等等。下面的示例展示了如何定义本地`PlatformTransactionManager`实现(在本例中,使用普通的 JDBC)。 你可以通过创建类似于以下的 Bean 来定义 JDBC`DataSource`: ``` ``` 然后,相关的`PlatformTransactionManager` Bean 定义引用了 ` 数据源’定义。它应该类似于以下示例: ``` ``` 如果在 爪哇 EE 容器中使用 JTA,则使用通过 JNDI 获得的容器`DataSource`,并结合 Spring 的`JtaTransactionManager`。下面的示例显示了 JTA 和 JNDI 查找版本的外观: ``` ``` `JtaTransactionManager`不需要了解`DataSource`(或任何其他特定资源),因为它使用容器的全局事务管理基础架构。 | |前面的`dataSource` Bean 定义使用了``名称空间中的
标记
。有关更多信息,请参见[The JEE Schema](integration.html#xsd-schemas-jee)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |如果你使用 JTA,那么你的事务管理器定义看起来应该是相同的,无论你使用的是什么数据访问技术,无论是 JDBC、 Hibernate JPA,还是任何其他受支持的技术。这是因为 JTA 事务是全局事务,
可以获取任何事务资源。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在所有 Spring 事务设置中,应用程序代码不需要更改。你可以仅通过更改配置来更改事务的管理方式,即使这种更改意味着从本地事务转移到全局事务,或者反之亦然。 #### 1.2.1. Hibernate 交易设置 还可以轻松地使用 Hibernate 本地事务,如以下示例所示。在这种情况下,你需要定义一个 Hibernate `LocalSessionFactoryBean`,你的应用程序代码可以使用它来获得 Hibernate `Session`实例。 `DataSource` Bean 的定义类似于前面显示的本地 JDBC 示例,因此在下面的示例中没有显示。 | |如果`DataSource`(由任何非 JTA 事务管理器使用)通过
JNDI 查找并由 爪哇 EE 容器管理,则它应该是非事务性的,因为
Spring 框架(而不是 爪哇 EE 容器)管理事务。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在这种情况下,`txManager` Bean 是`HibernateTransactionManager`类型的。就像`DataSourceTransactionManager`需要引用`DataSource`一样,“HibernateTransactionManager”需要引用`SessionFactory`。下面的示例声明`sessionFactory`和`txManager`bean: ``` org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml hibernate.dialect=${hibernate.dialect} ``` 如果使用 Hibernate 和 爪哇 EE 容器管理的 JTA 事务,则应该使用与上一个 JTA 示例中的 JDBC 相同的`JtaTransactionManager`,如下例所示。另外,建议 Hibernate 通过 JTA 的事务协调器以及可能的连接释放模式配置来了解 JTA: ``` org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml hibernate.dialect=${hibernate.dialect} hibernate.transaction.coordinator_class=jta hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT ``` 或者,你可以将`JtaTransactionManager`传递到`LocalSessionFactoryBean`中,以执行相同的默认值: ``` org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml hibernate.dialect=${hibernate.dialect} ``` ### 1.3.将资源与事务同步 如何创建不同的事务管理器,以及它们如何链接到需要同步到事务的相关资源(例如`DataSourceTransactionManager`到一个 JDBC`DataSource`,`HibernateTransactionManager`到一个 Hibernate `SessionFactory`,等等),现在应该已经很清楚了。本节描述了应用程序代码如何(通过使用持久性 API(如 JDBC、 Hibernate 或 JPA)直接或间接地)确保正确地创建、重用和清理这些资源。本节还讨论了如何(可选地)通过相关的`TransactionManager`触发事务同步。 #### 1.3.1.高层同步方法 首选的方法是使用 Spring 最高级别的基于模板的持久性集成 API,或者使用带有事务感知工厂 bean 或代理的本机 ORM API 来管理本机资源工厂。这些事务感知解决方案在内部处理资源的创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以完全关注于非样板持久性逻辑。通常,你使用本机 ORM API,或者通过使用`JdbcTemplate`对 JDBC 访问采用模板方法。这些解决方案将在本参考文档的后续部分中详细介绍。 #### 1.3.2.低水平同步方法 诸如`DataSourceUtils`(对于 JDBC)、`EntityManagerFactoryUtils`(对于 JPA)、`sessionFactoryutils’(对于 Hibernate)等类存在于较低的级别。当你希望应用程序代码直接处理本机持久性 API 的资源类型时,可以使用这些类来确保获得适当的 Spring 框架管理的实例,(可选地)同步事务,以及将过程中发生的异常正确地映射到一致的 API。 例如,在 JDBC 的情况下,你可以使用 Spring 的 `org.springframework.jdbc.datasource.datasourceutils’类,而不是在`DataSource`上调用`getConnection()`方法的传统 JDBC 方法,如下所示: ``` Connection conn = DataSourceUtils.getConnection(dataSource); ``` 如果现有事务已经有一个与其同步(链接)的连接,则返回该实例。否则,方法调用将触发新连接的创建,该连接(可选地)与任何现有事务同步,并可用于该相同事务的后续重用。如前所述,任何“sqlexception”都包装在 Spring 框架`CannotGetJdbcConnectionException`中,这是 Spring 框架的未检查`DataAccessException`类型的层次结构之一。这种方法为你提供了比从`SQLException`更容易获得的信息,并确保了跨数据库甚至跨不同持久性技术的可移植性。 该方法也可以在不使用 Spring 事务管理的情况下工作(事务同步是可选的),因此无论是否使用 Spring 用于事务管理,都可以使用它。 当然,一旦使用了 Spring 的 JDBC 支持、 JPA 支持或 Hibernate 支持,通常不会使用`DataSourceUtils`或其他助手类,因为与直接使用相关的 API 相比,通过 Spring 抽象进行工作要快乐得多。例如,如果你使用 Spring `JdbcTemplate`或 `jdbc.object’包来简化对 JDBC 的使用,那么正确的连接检索就会在幕后发生,并且你不需要编写任何特殊的代码。 #### 1.3.3.`TransactionAwareDataSourceProxy` 在最底层存在`TransactionAwareDataSourceProxy`类。这是目标`DataSource`的代理,该代理封装了目标`DataSource`,以添加对 Spring 管理的事务的感知。在这方面,它类似于由 爪哇 EE 服务器提供的事务性 JNDI“数据源”。 你几乎不需要或不想使用这个类,除非必须调用现有代码并传递标准的 JDBC`DataSource`接口实现。在这种情况下,该代码可能是可用的,但它参与了 Spring 管理的事务。你可以使用前面提到的更高级别的抽象来编写新代码。 ### 1.4.声明式事务管理 | |Spring 大多数框架用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合
非侵入式轻量级容器的理想。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring 框架的声明性事务管理通过 Spring 面向方面的编程( AOP)而成为可能。然而,由于事务性方面的代码来自 Spring 框架分布并且可以以样板方式使用, AOP 概念通常不必被理解以有效地使用该代码。 Spring 框架的声明式事务管理类似于 EJB CMT,因为你可以指定事务行为(或缺少事务行为),直至单个方法级别。如果有必要,可以在事务上下文中进行`setRollbackOnly()`调用。这两种类型的事务管理的区别在于: * 与绑定到 JTA 的 EJB CMT 不同, Spring 框架的声明式事务管理可以在任何环境中工作。它可以通过使用 JDBC、 JPA 或 Hibernate 调整配置文件来处理 JTA 事务或本地事务。 * 你可以将 Spring 框架声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类。 * Spring 框架提供了声明性[rollback rules](#transaction-declarative-rolling-back),这是一个没有 EJB 等价物的特性。提供了对回滚规则的编程支持和声明式支持。 * Spring 框架允许你通过使用 AOP 自定义事务行为。例如,你可以在事务回滚的情况下插入自定义行为。你还可以添加任意的建议以及事务性建议。使用 EJB CMT,你不能影响容器的事务管理,除非使用“setRollBackOnly()”。 * Spring 框架不支持跨远程调用传播事务上下文,就像高端应用服务器所做的那样。如果你需要此功能,我们建议你使用 EJB。然而,在使用这样的特性之前要仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。 回滚规则的概念很重要。它们允许你指定哪些异常(和 throwable)应该导致自动回滚。你可以在配置中,而不是在 爪哇 代码中,以声明式的方式指定此项。因此,尽管你仍然可以在`TransactionStatus`对象上调用`setRollbackOnly()`来回滚当前事务,但大多数情况下,你可以指定一条规则,即`MyApplicationException`必须始终导致回滚。此选项的显著优点是业务对象不依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。 虽然 EJB 容器的默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除`java.rmi.RemoteException`以外的检查异常)上自动回滚事务。虽然 Spring 声明性事务管理的默认行为遵循 EJB 约定(回滚仅在未检查的异常情况下是自动的),但定制这种行为通常是有用的。 #### 1.4.1.理解 Spring 框架的声明式事务实现 仅仅告诉你使用“@Transactional”注释对类进行注释,在配置中添加`@EnableTransactionManagement`,并期望你了解它是如何工作的,这是不够的。为了提供更深入的理解,本节将在与事务相关的问题的上下文中解释 Spring 框架的声明式事务基础结构的内部工作方式。 关于 Spring 框架的声明性事务支持,最重要的概念是启用了[via AOP proxies](core.html#aop-understanding-aop-proxies),并且事务建议由元数据(目前是基于 XML 或注释)驱动。 AOP 与事务性元数据的组合产生了 AOP 代理,该代理使用`TransactionInterceptor`并结合适当的`TransactionManager`实现来驱动围绕方法调用的事务。 | |Spring AOP 在[the AOP section](core.html#aop)中涵盖。| |---|----------------------------------------------------------| Spring Framework 的`TransactionInterceptor`为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回诸如`Publisher`或 Kotlin `Flow`之类的反应性类型的方法(或其中的一个子类型)符合进行反应性事务管理的条件。包括`void`在内的所有其他返回类型都使用强制事务管理的代码路径。 事务管理风格会影响所需的事务管理器。命令式事务需要`PlatformTransactionManager`,而反应式事务使用 `reactiveTransactionManager’实现。 | |`@Transactional`通常与由 `Platform TransactionManager’管理的线程绑定事务一起工作,将事务暴露于
当前执行线程内的所有数据访问操作。注意:这将*不是*传播到方法内新启动的线程


由`ReactiveTransactionManager`管理的反应式事务使用反应器上下文
而不是线程本地属性。因此,所有参与的数据访问
操作都需要在相同的反应性管道中的相同的反应器上下文中执行。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下图显示了在事务代理上调用方法的概念视图: ![tx](images/tx.png) #### 1.4.2.声明式事务实现示例 考虑下面的接口及其附带的实现。这个示例使用 `foo’和`Bar`类作为占位符,这样你就可以专注于事务使用,而不必关注特定的域模型。对于本例的目的,`DefaultFooService`类在每个实现的方法的主体中抛出`UnsupportedOperationException`实例的事实是好的。这种行为允许你看到正在创建的事务,然后响应“unsupportedOperationException”实例进行回滚。下面的清单显示了`FooService`接口: 爪哇 ``` // the service interface that we want to make transactional package x.y.service; public interface FooService { Foo getFoo(String fooName); Foo getFoo(String fooName, String barName); void insertFoo(Foo foo); void updateFoo(Foo foo); } ``` Kotlin ``` // the service interface that we want to make transactional package x.y.service interface FooService { fun getFoo(fooName: String): Foo fun getFoo(fooName: String, barName: String): Foo fun insertFoo(foo: Foo) fun updateFoo(foo: Foo) } ``` 下面的示例展示了前面接口的一个实现: 爪哇 ``` package x.y.service; public class DefaultFooService implements FooService { @Override public Foo getFoo(String fooName) { // ... } @Override public Foo getFoo(String fooName, String barName) { // ... } @Override public void insertFoo(Foo foo) { // ... } @Override public void updateFoo(Foo foo) { // ... } } ``` Kotlin ``` package x.y.service class DefaultFooService : FooService { override fun getFoo(fooName: String): Foo { // ... } override fun getFoo(fooName: String, barName: String): Foo { // ... } override fun insertFoo(foo: Foo) { // ... } override fun updateFoo(foo: Foo) { // ... } } ``` 假设`FooService`接口的前两个方法`getFoo(String)`和 `getfoo(字符串,字符串)’必须在具有只读语义的事务的上下文中运行,而其他方法`insertFoo(Foo)`和`updateFoo(Foo)`必须在具有读写语义的事务的上下文中运行。下面的几段将详细介绍以下配置: ``` ``` 检查前面的配置。它假定你希望创建一个事务性的服务对象`fooService` Bean。要应用的事务语义封装在``定义中。``定义为“所有以`get`开头的方法都将在只读事务的上下文中运行,所有其他方法都将使用默认事务语义运行”。将``标记的 `transactionmanager’属性设置为将驱动事务的 `transactionmanager’ Bean 的名称(在本例中,为 `txmanager’ Bean)。 | |如果要
中的`TransactionManager`线的 Bean 名称为`transactionManager`,则可以在事务通知
(``)中省略`transaction-manager`属性。如果要连接的`TransactionManager` Bean 中的
具有任何其他名称,则必须显式地使用`transaction-manager`属性,如前面的示例所示。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ``定义确保由 txadvice Bean 定义的事务通知在程序中的适当位置运行。首先,定义一个切入点,该切入点与`FooService`接口中定义的任何操作的执行相匹配。然后使用 advisor 将切入点与`txAdvice`关联起来。结果表明,在执行`fooServiceOperation`时,运行由`txAdvice`定义的通知。 在``元素中定义的表达式是 AspectJ 切入点表达式。有关 Spring 中的切入点表达式的更多详细信息,请参见[the AOP section](core.html#aop)。 一个常见的需求是使整个服务层具有事务性。实现此目的的最佳方法是更改切入点表达式,以匹配服务层中的任何操作。下面的示例展示了如何做到这一点: ``` ``` | |在前面的示例中,假设你的所有服务接口都是在`x.y.service`包中定义的
。有关更多详细信息,请参见[the AOP section](core.html#aop)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 既然我们已经分析了配置,那么你可能会问自己:“所有这些配置实际上做了什么?” 前面显示的配置用于围绕根据`fooService` Bean 定义创建的对象创建事务代理。该代理配置了事务通知,以便当在该代理上调用适当的方法时,根据与该方法关联的事务配置,启动、挂起、标记为只读等等。考虑以下测试驱动前面显示的配置的程序: 爪哇 ``` public final class Boot { public static void main(final String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); FooService fooService = ctx.getBean(FooService.class); fooService.insertFoo(new Foo()); } } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = ClassPathXmlApplicationContext("context.xml") val fooService = ctx.getBean("fooService") fooService.insertFoo(Foo()) } ``` 运行前一个程序的输出应该类似于以下内容(为清楚起见,对`DefaultFooService`类的 `insertfoo(..)’方法抛出的`UnsupportedOperationException`的 log4j 输出和堆栈跟踪进行了截断): ``` [AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors [JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService] [TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo [DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo] [DataSourceTransactionManager] - Acquired Connection [[email protected]] for JDBC transaction [RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException [TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException] [DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [[email protected]] [DataSourceTransactionManager] - Releasing JDBC Connection after transaction [DataSourceUtils] - Returning JDBC Connection to DataSource Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14) at $Proxy0.insertFoo(Unknown Source) at Boot.main(Boot.java:11) ``` 要使用反应式事务管理,代码必须使用反应式类型。 | |Spring 框架使用`ReactiveAdapterRegistry`来确定方法
的返回类型是否是反应性的。| |---|--------------------------------------------------------------------------------------------------------------| 下面的清单显示了以前使用的`FooService`的修改版本,但这次代码使用了反应类型: 爪哇 ``` // the reactive service interface that we want to make transactional package x.y.service; public interface FooService { Flux getFoo(String fooName); Publisher getFoo(String fooName, String barName); Mono insertFoo(Foo foo); Mono updateFoo(Foo foo); } ``` Kotlin ``` // the reactive service interface that we want to make transactional package x.y.service interface FooService { fun getFoo(fooName: String): Flow fun getFoo(fooName: String, barName: String): Publisher fun insertFoo(foo: Foo) : Mono fun updateFoo(foo: Foo) : Mono } ``` 下面的示例展示了前面接口的一个实现: 爪哇 ``` package x.y.service; public class DefaultFooService implements FooService { @Override public Flux getFoo(String fooName) { // ... } @Override public Publisher getFoo(String fooName, String barName) { // ... } @Override public Mono insertFoo(Foo foo) { // ... } @Override public Mono updateFoo(Foo foo) { // ... } } ``` Kotlin ``` package x.y.service class DefaultFooService : FooService { override fun getFoo(fooName: String): Flow { // ... } override fun getFoo(fooName: String, barName: String): Publisher { // ... } override fun insertFoo(foo: Foo): Mono { // ... } override fun updateFoo(foo: Foo): Mono { // ... } } ``` 对于事务边界和事务属性定义,命令式事务管理和反应式事务管理共享相同的语义。命令式事务和反应式事务之间的主要区别在于后者的延迟性。`TransactionInterceptor`使用事务操作符装饰返回的反应类型,以开始并清理事务。因此,调用事务性反应方法将实际事务管理推迟到激活反应类型处理的订阅类型。 反应式事务管理的另一个方面涉及数据转义,这是编程模型的自然结果。 命令式事务的方法返回值在方法成功终止时从事务性方法返回,以便部分计算的结果不会逃脱方法闭包。 Active Transaction 方法返回一个 Active 包装器类型,该类型表示一个计算序列以及开始和完成计算的承诺。 `Publisher`可以在事务正在进行但不一定完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保调用代码中的完成和缓冲结果。 #### 1.4.3.回滚声明性事务 上一节概述了如何在应用程序中以声明方式为类(通常是服务层类)指定事务设置的基础知识。本节介绍如何以简单的声明式方式控制事务的回滚。 向 Spring 框架的事务基础结构指示要回滚事务工作的推荐方法是,从当前在事务上下文中执行的代码中抛出`Exception`。 Spring 框架的事务基础设施代码捕获任何未处理的`Exception`,因为它在调用堆栈中冒泡,并决定是否将事务标记为回滚。 在其默认配置中, Spring 框架的事务基础设施代码仅在运行时未检查异常的情况下标记用于回滚的事务。也就是说,当抛出的异常是`RuntimeException`的实例或子类时。(默认情况下,“错误”实例也会导致回滚)。从事务方法抛出的已检查异常不会在默认配置中导致回滚。 你可以准确地配置哪些`Exception`类型标记了回滚事务,包括选中的异常。下面的 XML 片段演示了如何为选中的、特定于应用程序的`Exception`类型配置回滚: ``` ``` 如果你不希望在抛出异常时回滚事务,也可以指定“没有回滚规则”。下面的示例告诉 Spring 框架的事务基础结构,即使面对未处理的`InstrumentNotFoundException`,也要提交伴随的事务: ``` ``` Spring 框架的事务基础设施捕捉到异常并参考配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则胜出。因此,在以下配置的情况下,除了`InstrumentNotFoundException`以外的任何异常都会导致附带事务的回滚: ``` ``` 你还可以通过编程的方式指示所需的回滚。尽管很简单,但这个过程是非常具有侵入性的,并且将你的代码与 Spring 框架的事务基础结构紧密地结合在一起。下面的示例展示了如何以编程方式指示所需的回滚: 爪哇 ``` public void resolvePosition() { try { // some business logic... } catch (NoProductInStockException ex) { // trigger rollback programmatically TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } } ``` Kotlin ``` fun resolvePosition() { try { // some business logic... } catch (ex: NoProductInStockException) { // trigger rollback programmatically TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } } ``` 如果可能的话,我们强烈建议你使用声明式方法进行回滚。如果你绝对需要编程回滚,它是可用的,但是它的使用与实现一个干净的基于 POJO 的体系结构是背道而驰的。 #### 1.4.4.为不同的 bean 配置不同的事务语义 ### 考虑这样的场景:你有许多服务层对象,并且希望对每个对象应用完全不同的事务配置。可以通过使用不同的`pointcut`和 `advice-ref’属性值定义不同的``元素来实现。 作为比较,首先假设你的所有服务层类都是在根`x.y.service`包中定义的。要使在该包(或子包)中定义的类的实例以及名称以`Service`结尾的所有 bean 具有默认的事务配置,你可以编写以下内容: ``` ``` 下面的示例展示了如何配置具有完全不同的事务设置的两个不同的 bean: ``` ``` #### 1.4.5.\设置 本节总结了你可以通过使用``标记来指定的各种事务设置。默认的``设置是: * [传播设定](#tx-propagation)是`REQUIRED.` * 隔离级别为`DEFAULT.` * 事务是可读写的。 * 事务超时默认为基础事务系统的默认超时,如果不支持超时,则无超时。 * 任何`RuntimeException`都会触发回滚,而任何选中的`Exception`都不会。 你可以更改这些默认设置。下表总结了嵌套在``和``标签中的``标签的各种属性: | Attribute |Required?| Default |说明| |-----------------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | Yes | |将事务属性与之关联的方法名。可以使用
通配符(\*)字符将相同的事务属性
设置与许多方法关联(例如,`get*`,`handle*`,`on*Event`,以及
forth)。| | `propagation` | No |`REQUIRED`|事务传播行为。| | `isolation` | No |`DEFAULT` |事务隔离级别。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播设置。| | `timeout` | No | \-1 |事务超时(秒)。仅适用于传播`REQUIRED`或`REQUIRES_NEW`。| | `read-only` | No | false |读写与只读事务。仅适用于`REQUIRED`或`REQUIRES_NEW`。| | `rollback-for` | No | |触发回滚的`Exception`实例的逗号分隔列表。例如,“com.foo.mybusinessexception,servletexception”。| |`no-rollback-for`| No | |不触发回滚的`Exception`实例的逗号分隔列表。例如,“com.foo.mybusinessexception,servletexception”。| #### 1.4.6.使用`@Transactional` 除了事务配置的基于 XML 的声明式方法之外,还可以使用基于注释的方法。直接在 爪哇 源代码中声明事务语义会使声明更接近受影响的代码。不存在太大的过度耦合的危险,因为本来打算在交易中使用的代码几乎总是以这种方式部署的。 | |标准的`javax.transaction.Transactional`注释也被支持为 Spring 自己的注释的
插入替换。有关更多详细信息,请参阅 JTA1.2 文档
。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 使用`@Transactional`注释所提供的易用性最好用一个示例来说明,下面的文本对此进行了解释。考虑以下类定义: 爪哇 ``` // the service class that we want to make transactional @Transactional public class DefaultFooService implements FooService { @Override public Foo getFoo(String fooName) { // ... } @Override public Foo getFoo(String fooName, String barName) { // ... } @Override public void insertFoo(Foo foo) { // ... } @Override public void updateFoo(Foo foo) { // ... } } ``` Kotlin ``` // the service class that we want to make transactional @Transactional class DefaultFooService : FooService { override fun getFoo(fooName: String): Foo { // ... } override fun getFoo(fooName: String, barName: String): Foo { // ... } override fun insertFoo(foo: Foo) { // ... } override fun updateFoo(foo: Foo) { // ... } } ``` 在类级别上使用,如上面所述,该注释表示声明类的所有方法(以及其子类)的默认值。或者,每个方法都可以单独注释。有关 Spring 考虑事务性的方法的更多详细信息,请参见[Method visibility and `@Transactional`](#transaction-declarative-annotations-method-visibility)。请注意,类级注释不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级注释。 当像上面这样的 POJO 类在 Spring 上下文中被定义为 Bean 时,可以在`@Configuration`类中通过`@EnableTransactionManagement`注释使 Bean 实例具有事务性。有关详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/annotation/EnableTransactionManagement.html)。 在 XML 配置中,``标记提供了类似的便利: ``` (1) ``` |**1**|使 Bean 实例具有事务性的行。| |-----|----------------------------------------------------| | |如果要连接的`TransactionManager`的 Bean 名称为 `TransactionManager’,则可以省略`transaction-manager`标记中的`transaction-manager`属性。如果要依赖注入的`TransactionManager` Bean 中的
具有任何其他名称,则必须使用`transaction-manager`属性,如在
前面的示例中所示。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 反应式事务方法使用反应式返回类型,而不是命令式编程安排,如下所示: 爪哇 ``` // the reactive service class that we want to make transactional @Transactional public class DefaultFooService implements FooService { @Override public Publisher getFoo(String fooName) { // ... } @Override public Mono getFoo(String fooName, String barName) { // ... } @Override public Mono insertFoo(Foo foo) { // ... } @Override public Mono updateFoo(Foo foo) { // ... } } ``` Kotlin ``` // the reactive service class that we want to make transactional @Transactional class DefaultFooService : FooService { override fun getFoo(fooName: String): Flow { // ... } override fun getFoo(fooName: String, barName: String): Mono { // ... } override fun insertFoo(foo: Foo): Mono { // ... } override fun updateFoo(foo: Foo): Mono { // ... } } ``` 注意,对于返回的`Publisher`有关于无功流抵消信号的特殊考虑因素。有关更多详细信息,请参见“使用 TransactionOperator”下的[取消信号](#tx-prog-operator-cancel)小节。 | |方法可见性和`@Transactional`

当你使用具有 Spring 的标准配置的事务代理时,你应该只对具有
可见性的方法应用`@Transactional`注释。如果你使用
注释`protected`、`private`或使用`@Transactional`注释的包-可见方法,则不会引发错误,但是注释的方法不显示已配置的
事务设置。如果需要对非公共方法进行注释,请考虑以下段落中的技巧,用于基于类的代理,或者考虑使用 AspectJ 编译时或加载时编织(稍后将进行说明)。在类中使用时,`protected`或
包-visible 方法也可以通过
注册自定义的`transactionAttributeSource` Bean 来使基于类的代理具有事务性,就像下面的示例一样。
注意,基于接口的代理中的事务方法必须始终是“公共的”并在 proxied 接口中进行了定义。

```
/**
* Register a custom AnnotationTransactionAttributeSource with the
*PublicMethodsOnly 标志设置为 false,以启用对
* protected and package-private @Transactional methods in
*基于类的代理的支持。
@see yTransactionConfiguration#TransactioneManageR=”()“/>527”/><<503>>>[proxtActeConteFramework=527"/>[参见[事务管理](testing.html#testcontext-tx)中的测试
章节中的示例。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以将`@Transactional`注释应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在`@Transactional`注释并不足以激活事务行为。`@Transactional`注释仅仅是元数据,它可以被一些运行时基础设施(`@Transactional`)使用,并且可以使用元数据来配置具有事务行为的适当 bean。在前面的示例中,``元素切换事务行为。 | |Spring 团队建议你只使用`@Transactional`注释来注释具体的类(和
具体类的方法),而不是注释接口,
你当然可以将`@Transactional`注释放在接口(或接口
方法)上,但是,只有当你使用基于接口的
代理时,这才能正常工作。爪哇 注释不是从接口继承而来的,这意味着,如果使用基于类的代理(`proxy-target-class=“true”`)或基于编织的
方面(`mode=“aspectj”`),代理
和编织基础设施将无法识别事务设置,并且该对象不包装在事务性代理中。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |在代理模式(这是默认模式)中,只有通过
代理进入的外部方法调用才会被拦截。这意味着自我调用(实际上,在
中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的
事务,即使调用的方法被标记为`@Transactional`。此外,
代理必须完全初始化以提供预期的行为,因此你不应该在初始化代码中依赖
这个特性——例如,在`@PostConstruct`方法中。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果你希望使用事务来包装自调用,请考虑使用 AspectJ 模式(参见下表中的`mode`属性)。在这种情况下,首先不存在代理。相反,目标类被编织(也就是说,它的字节码被修改)以支持在任何类型的方法上的`@Transactional`运行时行为。 | XML Attribute | Annotation Attribute | Default |说明| |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |`transaction-manager`|N/A (see [`TransactionManagementConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/annotation/TransactionManagementConfigurer.html) javadoc)| `transactionManager` |要使用的事务管理器的名称。仅当事务
管理器的名称不是`transactionManager`时才需要,如前面的示例所示。| | `mode` | `mode` | `proxy` |缺省模式处理要通过使用 Spring 的 AOP
框架来代理的带注释的 bean(遵循代理语义,如前所述,应用于仅通过代理进入的方法调用
)。替代模式使用 Spring 的 AspectJ 事务方面来编织
受影响的类,修改目标类
字节码以应用于任何类型的方法调用。AspectJ 的编织需要 Spring-Aspects. jar 中的 Classpath 以及具有启用的加载时编织(或编译时
编织)。(有关如何设置加载时编织的详细信息,请参见[Spring configuration](core.html#aop-aj-ltw-spring)。| |`proxy-target-class` | `proxyTargetClass` | `false` |仅适用于`proxy`模式。控制为使用`@Transactional`注释的类创建的事务代理类型
。如果“proxy-target-class”属性设置为`true`,则创建基于类的代理。
如果`proxy-target-class`是`false`,或者如果省略了该属性,则创建标准的 JDK
基于接口的代理。(有关不同代理类型的详细检查,请参见[代理机制](core.html#aop-proxying)。| | `order` | `order` |`Ordered.LOWEST_PRECEDENCE`|定义应用于带“@transactional”注释的 bean 的事务通知的顺序。(有关与 AOP 通知的排序相关的规则的更多信息,请参见。)没有指定的排序意味着由 AOP 子系统确定通知的顺序。| | |处理`@Transactional`注释的默认通知模式是`proxy`,
,这仅允许通过代理拦截调用。在
相同的类内的本地调用不能以这种方式被拦截。对于更高级的拦截模式,
可以考虑结合编译时或加载时编织切换到`aspectj`模式。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |`proxy-target-class`属性控制为使用`@Transactional`注释的类创建的事务代理类型
。如果将“proxy-target-class”设置为`true`,则创建基于类的代理。如果“proxy-target-class”是`false`,或者省略了该属性,则创建标准的基于接口的 JDK
代理。(有关不同代理类型的讨论,请参见[代理机制](core.html#aop-proxying)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |`@EnableTransactionManagement`和``仅在定义它们的相同应用程序上下文中的 bean 上查找 `@transactional’。
这意味着,如果将注释驱动的配置放在`WebApplicationContext`的`DispatcherServlet`中,它只在你的控制器`@Transactional`中检查`@Transactional`bean,而不是在你的服务中。有关更多信息,请参见[MVC](web.html#mvc-servlet)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在计算方法的事务设置时,派生最多的位置优先。在下面的示例中,`DefaultFooService`类在类级别上使用只读事务的设置进行注释,但是在同一个类中的`updateFoo(Foo)`方法上的 `@Transactional’注释优先于在类级别上定义的事务设置。 爪哇 ``` @Transactional(readOnly = true) public class DefaultFooService implements FooService { public Foo getFoo(String fooName) { // ... } // these settings have precedence for this method @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW) public void updateFoo(Foo foo) { // ... } } ``` Kotlin ``` @Transactional(readOnly = true) class DefaultFooService : FooService { override fun getFoo(fooName: String): Foo { // ... } // these settings have precedence for this method @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW) override fun updateFoo(foo: Foo) { // ... } } ``` ##### `@Transactional`设置 `@Transactional`注释是元数据,它指定接口、类或方法必须具有事务语义(例如,“在调用该方法时启动一个全新的只读事务,挂起任何现有事务”)。默认的`@Transactional`设置如下: * 传播设置为`PROPAGATION_REQUIRED.` * 隔离级别`ISOLATION_DEFAULT.` * 事务是可读写的。 * 事务超时默认为基础事务系统的默认超时,如果不支持超时,则为无超时。 * 任何`RuntimeException`都会触发回滚,而任何选中的`Exception`都不会。 你可以更改这些默认设置。下表总结了`@Transactional`注释的各种属性: | Property | Type |说明| |--------------------------------------------------|-----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| |[value](#tx-multiple-tx-mgrs-with-attransactional)| `String` |指定要使用的事务管理器的可选限定符。| | [propagation](#tx-propagation) | `enum`: `Propagation` |可选的传播设置。| | `isolation` | `enum`: `Isolation` |可选隔离级别。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播值。| | `timeout` | `int` (in seconds of granularity) |可选事务超时。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播值。| | `readOnly` | `boolean` |读写与只读事务。仅适用于`REQUIRED`或`REQUIRES_NEW`的值。| | `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` |必须引起回滚的异常类的可选数组.| | `rollbackForClassName` | Array of class names. The classes must be derived from `Throwable.` |必须引起回滚的异常类名称的可选数组。| | `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` |不能导致回滚的异常类的可选数组.| | `noRollbackForClassName` | Array of `String` class names, which must be derived from `Throwable.` |不能导致回滚的异常类名称的可选数组。| | `label` |Array of `String` labels to add an expressive description to the transaction.|标签可以由事务管理器进行评估,以将
实现特定的行为与实际事务相关联。| 目前,你无法对事务的名称进行显式控制,其中“name”是指事务监视器(如果适用)(例如,WebLogic 的事务监视器)和日志输出中出现的事务名称。对于声明式事务,事务名称始终是完全限定类名称 +`.`+ 事务建议类的方法名称。例如,如果`BusinessService`类的 `handlePayment(..)’方法启动了一个事务,那么事务的名称将是:`com.example.BusinessService.handlePayment`。 ##### 具有`@Transactional`的多个事务管理器 Spring 大多数应用程序只需要一个事务管理器,但是可能存在希望在单个应用程序中有多个独立事务管理器的情况。你可以使用“@Transactional”注释的`value`或`transactionManager`属性来指定要使用的“TransactionManager”的标识。这可以是 Bean 名称,也可以是事务管理器 Bean 的限定符值。例如,使用限定符表示法,你可以在应用程序上下文中将以下 爪哇 代码与以下事务管理器 Bean 声明结合起来: 爪哇 ``` public class TransactionalService { @Transactional("order") public void setSomething(String name) { ... } @Transactional("account") public void doSomething() { ... } @Transactional("reactive-account") public Mono doSomethingReactive() { ... } } ``` Kotlin ``` class TransactionalService { @Transactional("order") fun setSomething(name: String) { // ... } @Transactional("account") fun doSomething() { // ... } @Transactional("reactive-account") fun doSomethingReactive(): Mono { // ... } } ``` 下面的清单显示了 Bean 项声明: ``` ... ... ... ``` 在这种情况下,`TransactionalService`上的各个方法在单独的事务管理器下运行,由`order`、`account`和`reactive-account`限定符区分。如果没有找到特别限定的`TransactionManager` Bean,则仍然使用默认的``目标 Bean 名称`transactionManager`。 ##### 自定义合成注释 如果你发现在许多不同的方法上重复使用与`@Transactional`相同的属性,[Spring’s meta-annotation support](core.html#beans-meta-annotations)允许你为特定的用例定义自定义组合注释。例如,考虑以下注释定义: 爪哇 ``` @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Transactional(transactionManager = "order", label = "causal-consistency") public @interface OrderTx { } @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Transactional(transactionManager = "account", label = "retryable") public @interface AccountTx { } ``` Kotlin ``` @Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @Transactional(transactionManager = "order", label = ["causal-consistency"]) annotation class OrderTx @Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @Transactional(transactionManager = "account", label = ["retryable"]) annotation class AccountTx ``` 前面的注释让我们将上一节的示例写如下: 爪哇 ``` public class TransactionalService { @OrderTx public void setSomething(String name) { // ... } @AccountTx public void doSomething() { // ... } } ``` Kotlin ``` class TransactionalService { @OrderTx fun setSomething(name: String) { // ... } @AccountTx fun doSomething() { // ... } } ``` 在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但也可以包括传播行为、回滚规则、超时和其他功能。 #### 1.4.7.事务传播 本节描述 Spring 中事务传播的一些语义。请注意,本节不是事务传播的适当介绍。相反,它详细介绍了 Spring 中有关事务传播的一些语义。 在 Spring-管理事务中,要注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种区别。 ##### 理解`PROPAGATION_REQUIRED` ![需要 TX 道具](images/tx_prop_required.png) `PROPAGATION_REQUIRED`强制执行一个物理事务,如果当前范围还不存在事务,则在本地执行,或者参与为更大范围定义的现有“外部”事务。这是相同线程内的公共调用堆栈安排中的一个很好的默认设置(例如,一个服务 facade,它将委托给几个存储库方法,在这些存储库中,所有底层资源都必须参与服务级事务)。 | |默认情况下,参与事务将加入外部作用域的特性,
默默地忽略本地隔离级别超时值,或只读标志(如果有的话)。
如果你希望在参与
具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务`validateExistingTransactions`管理器上将`true`标志切换为`true`。这种不宽松的模式还
拒绝只读不匹配(即,试图参与
只读外部作用域的内部读写事务)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 当传播设置`PROPAGATION_REQUIRED`时,将为应用该设置的每个方法创建一个逻辑事务作用域。每个这样的逻辑事务作用域可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准`PROPAGATION_REQUIRED`行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务范围中设置的只回滚标记确实会影响外部事务实际提交的机会。 但是,在内部事务作用域设置仅回滚标记的情况下,外部事务本身并未决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。这时会抛出相应的“unexpectedRollbackException”。这是一种预期的行为,这样事务的调用者就永远不会被误导,以为提交是在实际上没有执行的情况下执行的。因此,如果内部事务(外部调用方不知道该事务)静默地将事务标记为只回滚,则外部调用方仍然调用 commit。外部调用者需要接收`UnexpectedRollbackException`,以清楚地表明执行了回滚。 ##### 理解`PROPAGATION_REQUIRES_NEW` ![TX Prop 需要新的](images/tx_prop_requires_new.png) `PROPAGATION_REQUIRES_NEW`,与`PROPAGATION_REQUIRED`相反,总是对每个受影响的事务范围使用独立的物理事务,而不是参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务不受内部事务的回滚状态的影响,内部事务的锁在完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不会继承外部事务的特征。 ##### 理解`PROPAGATION_NESTED` `PROPAGATION_NESTED`使用一个具有多个保存点的单个物理事务,它可以回滚到这些保存点。这样的部分回滚让内部事务作用域触发其作用域的回滚,外部事务能够继续物理事务,尽管有些操作已经回滚。该设置通常映射到 JDBC 保存点上,因此它仅适用于 JDBC 资源事务。参见 Spring 的[“DataSourceTransactionManager”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html)。 #### 1.4.8.为交易业务提供咨询 假设你希望同时运行事务操作和一些基本的分析建议。在``的上下文中,如何实现这一点? 调用`updateFoo(Foo)`方法时,你希望看到以下操作: * 配置的分析方面开始。 * 事务性建议运行。 * 被建议对象上的方法运行。 * 事务提交。 * 分析方面报告了整个事务方法调用的确切持续时间。 | |本章不涉及对 AOP 进行任何详细的解释(除非它
适用于交易)。关于 AOP
配置和 AOP 一般的详细覆盖,请参见[AOP](core.html#aop)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的代码展示了前面讨论的简单分析方面: 爪哇 ``` package x.y; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.util.StopWatch; import org.springframework.core.Ordered; public class SimpleProfiler implements Ordered { private int order; // allows us to control the ordering of advice public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } // this method is the around advice public Object profile(ProceedingJoinPoint call) throws Throwable { Object returnValue; StopWatch clock = new StopWatch(getClass().getName()); try { clock.start(call.toShortString()); returnValue = call.proceed(); } finally { clock.stop(); System.out.println(clock.prettyPrint()); } return returnValue; } } ``` Kotlin ``` class SimpleProfiler : Ordered { private var order: Int = 0 // allows us to control the ordering of advice override fun getOrder(): Int { return this.order } fun setOrder(order: Int) { this.order = order } // this method is the around advice fun profile(call: ProceedingJoinPoint): Any { var returnValue: Any val clock = StopWatch(javaClass.name) try { clock.start(call.toShortString()) returnValue = call.proceed() } finally { clock.stop() println(clock.prettyPrint()) } return returnValue } } ``` 建议的排序是通过`Ordered`接口控制的。有关建议订购的详细信息,请参见[Advice ordering](core.html#aop-ataspectj-advice-ordering)。 以下配置创建了一个`fooService` Bean,该配置具有按所需顺序应用于它的分析和事务方面: ``` ``` 你可以以类似的方式配置任意数量的附加方面。 下面的示例创建了与前两个示例相同的设置,但使用了纯 XML 声明式方法: ``` ``` 上述配置的结果是一个`fooService` Bean,它具有按该顺序应用于它的分析和事务方面。如果你希望在进入事务建议之后和退出事务建议之前运行分析建议,则可以交换分析方面 Bean 的`order`属性的值,使其高于事务建议的订单值。 你可以以类似的方式配置其他方面。 #### 1.4.9.使用`@Transactional`与 AspectJ 你还可以通过 AspectJ 方面在 Spring 容器之外使用 Spring 框架的`@Transactional`支持。为此,首先使用`@Transactional`注释对类(以及可选的类的方法)进行注释,然后使用在 ` Spring-Aspects. jar ` 文件中定义的 `org.SpringFramework.Transaction.AspectJ.AnnotationTransactionAspect’链接(编织)应用程序。你还必须使用事务管理器配置方面。你可以使用 Spring 框架的 IOC 容器来处理依赖关系-注入方面。配置事务管理方面的最简单方法是使用``元素,并将`mode`属性指定为`aspectj`,如[Using `@Transactional`](#transaction-declarative-annotations)中所述。因为我们在这里关注的是在 Spring 容器之外运行的应用程序,所以我们向你展示了如何以编程方式完成它。 | |在继续之前,你可能需要分别阅读[Using `@Transactional`](#transaction-declarative-annotations)和[AOP](core.html#aop)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了如何创建事务管理器并配置“AnnotationTransactionAspect”来使用它: 爪哇 ``` // construct an appropriate transaction manager DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource()); // configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager); ``` Kotlin ``` // construct an appropriate transaction manager val txManager = DataSourceTransactionManager(getDataSource()) // configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods AnnotationTransactionAspect.aspectOf().transactionManager = txManager ``` | |当你使用这个方面时,你必须注释实现类(或该类中的方法
或两者),而不是该类实现的接口(如果有的话)。AspectJ
遵循 爪哇 的规则,即接口上的注释不会被继承。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 类上的`@Transactional`注释指定了类中任何公共方法的执行的默认事务语义。 类中方法上的`@Transactional`注释重写了类注释给出的默认事务语义(如果存在的话)。无论可见性如何,你都可以对任何方法进行注释。 要使用`AnnotationTransactionAspect`编织应用程序,你必须使用 AspectJ 构建应用程序(参见[AspectJ 开发指南](https://www.eclipse.org/aspectj/doc/released/devguide/index.html))或使用加载时编织。请参阅[Load-time weaving with AspectJ in the Spring Framework](core.html#aop-aj-ltw),以了解有关使用 AspectJ 进行加载时编织的讨论。 ### 1.5.程序化事务管理 Spring 框架提供了两种程序化事务管理的方法,通过使用: * `TransactionTemplate`或`TransactionalOperator`。 * 直接实现`TransactionManager`。 Spring 团队通常推荐`TransactionTemplate`用于命令流中的程序化事务管理,而`TransactionalOperator`用于反应性代码。第二种方法类似于使用 JTA`UserTransaction`API,尽管异常处理不那么麻烦。 #### 1.5.1.使用`TransactionTemplate` `TransactionTemplate`采用了与其他 Spring 模板相同的方法,例如`JdbcTemplate`。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。 | |正如下面的示例所示,使用`TransactionTemplate`绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 必须在事务上下文中运行并显式使用“TransactionTemplate”的应用程序代码类似于下一个示例。作为应用程序开发人员,你可以编写`TransactionCallback`实现(通常表示为匿名内部类),其中包含你需要在事务上下文中运行的代码。然后,你可以将自定义`TransactionCallback`的实例传递给在`TransactionTemplate`上公开的 `execute(..)’方法。下面的示例展示了如何做到这一点: 爪哇 ``` public class SimpleService implements Service { // single TransactionTemplate shared amongst all methods in this instance private final TransactionTemplate transactionTemplate; // use constructor-injection to supply the PlatformTransactionManager public SimpleService(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); } public Object someServiceMethod() { return transactionTemplate.execute(new TransactionCallback() { // the code in this method runs in a transactional context public Object doInTransaction(TransactionStatus status) { updateOperation1(); return resultOfUpdateOperation2(); } }); } } ``` Kotlin ``` // use constructor-injection to supply the PlatformTransactionManager class SimpleService(transactionManager: PlatformTransactionManager) : Service { // single TransactionTemplate shared amongst all methods in this instance private val transactionTemplate = TransactionTemplate(transactionManager) fun someServiceMethod() = transactionTemplate.execute { updateOperation1() resultOfUpdateOperation2() } } ``` 如果没有返回值,你可以使用带有匿名类的方便的`TransactionCallbackWithoutResult`类,如下所示: 爪哇 ``` transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { updateOperation1(); updateOperation2(); } }); ``` Kotlin ``` transactionTemplate.execute(object : TransactionCallbackWithoutResult() { override fun doInTransactionWithoutResult(status: TransactionStatus) { updateOperation1() updateOperation2() } }) ``` 回调中的代码可以通过调用所提供的`TransactionStatus`对象上的 `setRollBackOnly()’方法回滚事务,如下所示: 爪哇 ``` transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { try { updateOperation1(); updateOperation2(); } catch (SomeBusinessException ex) { status.setRollbackOnly(); } } }); ``` Kotlin ``` transactionTemplate.execute(object : TransactionCallbackWithoutResult() { override fun doInTransactionWithoutResult(status: TransactionStatus) { try { updateOperation1() updateOperation2() } catch (ex: SomeBusinessException) { status.setRollbackOnly() } } }) ``` ##### 指定事务设置 你可以在`TransactionTemplate`上以编程方式或在配置中指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,`TransactionTemplate`实例具有[默认事务设置](#transaction-declarative-txadvice-settings)。下面的示例显示了对特定`TransactionTemplate:`的事务设置的程序化定制。 爪哇 ``` public class SimpleService implements Service { private final TransactionTemplate transactionTemplate; public SimpleService(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); // the transaction settings can be set here explicitly if so desired this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); this.transactionTemplate.setTimeout(30); // 30 seconds // and so forth... } } ``` Kotlin ``` class SimpleService(transactionManager: PlatformTransactionManager) : Service { private val transactionTemplate = TransactionTemplate(transactionManager).apply { // the transaction settings can be set here explicitly if so desired isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds // and so forth... } } ``` 下面的示例通过使用 Spring XML 配置定义带有一些自定义事务设置的`TransactionTemplate`: ``` ``` 然后,你可以将`sharedTransactionTemplate`注入到所需的尽可能多的服务中。 最后,`TransactionTemplate`类的实例是线程安全的,因为实例不维护任何会话状态。`TransactionTemplate`实例确实保持配置状态。因此,虽然许多类可能共享`TransactionTemplate`的单个实例,但是如果一个类需要使用具有不同设置(例如,不同的隔离级别)的`TransactionTemplate`,则需要创建两个不同的`TransactionTemplate`实例。 #### 1.5.2.使用`TransactionOperator` `TransactionOperator`遵循类似于其他无功运算符的运算符设计。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。 | |正如下面的示例所示,使用`TransactionOperator`绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 必须在事务上下文中运行并显式使用`TransactionOperator`的应用程序代码类似于下一个示例: 爪哇 ``` public class SimpleService implements Service { // single TransactionOperator shared amongst all methods in this instance private final TransactionalOperator transactionalOperator; // use constructor-injection to supply the ReactiveTransactionManager public SimpleService(ReactiveTransactionManager transactionManager) { this.transactionOperator = TransactionalOperator.create(transactionManager); } public Mono someServiceMethod() { // the code in this method runs in a transactional context Mono update = updateOperation1(); return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional); } } ``` Kotlin ``` // use constructor-injection to supply the ReactiveTransactionManager class SimpleService(transactionManager: ReactiveTransactionManager) : Service { // single TransactionalOperator shared amongst all methods in this instance private val transactionalOperator = TransactionalOperator.create(transactionManager) suspend fun someServiceMethod() = transactionalOperator.executeAndAwait { updateOperation1() resultOfUpdateOperation2() } } ``` `TransactionalOperator`可以通过两种方式使用: * 操作员风格,使用项目反应器类型(“mono.as”) * 其他情况的回调样式(`transactionaloperator.execute(transactioncallback)`) 回调中的代码可以通过在提供的`ReactiveTransaction`对象上调用`setRollbackOnly()`方法回滚事务,如下所示: 爪哇 ``` transactionalOperator.execute(new TransactionCallback<>() { public Mono doInTransaction(ReactiveTransaction status) { return updateOperation1().then(updateOperation2) .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly()); } } }); ``` Kotlin ``` transactionalOperator.execute(object : TransactionCallback() { override fun doInTransactionWithoutResult(status: ReactiveTransaction) { updateOperation1().then(updateOperation2) .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly()) } }) ``` ##### Cancel Signals 在反应流中,`Subscriber`可以取消其`Subscription`并停止其 `publisher’。Project Reactor 中的操作员以及其他库中的操作员,例如`next()`、`take(long)’、`timeout(Duration)`,以及其他库中的操作员可以发出取消通知。没有办法知道取消的原因,无论是由于错误还是仅仅是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑事务“发布者”下游使用的操作符。特别是在 a`Flux`或其它多值`Publisher`的情况下,必须消耗完整的输出以允许事务完成。 ##### 指定事务设置 你可以为`TransactionalOperator`指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,“TransactionalOperator”实例具有[默认事务设置](#transaction-declarative-txadvice-settings)。下面的示例显示了针对特定的“TransactionalOperator”的事务设置的定制: Java ``` public class SimpleService implements Service { private final TransactionalOperator transactionalOperator; public SimpleService(ReactiveTransactionManager transactionManager) { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); // the transaction settings can be set here explicitly if so desired definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); definition.setTimeout(30); // 30 seconds // and so forth... this.transactionalOperator = TransactionalOperator.create(transactionManager, definition); } } ``` Kotlin ``` class SimpleService(transactionManager: ReactiveTransactionManager) : Service { private val definition = DefaultTransactionDefinition().apply { // the transaction settings can be set here explicitly if so desired isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds // and so forth... } private val transactionalOperator = TransactionalOperator(transactionManager, definition) } ``` #### 1.5.3.使用`TransactionManager` 下面的部分解释命令式事务管理器和反应式事务管理器的编程使用。 ##### 使用`PlatformTransactionManager` 对于命令式事务,你可以直接使用“org.springframework.transactionmanager.platform transactionmanager”来管理你的事务。要做到这一点,通过 Bean 引用将你使用的`PlatformTransactionManager`的实现传递给你的 Bean。然后,通过使用`TransactionDefinition`和 `TransactionStatus’对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点: Java ``` DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can be done only programmatically def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(def); try { // put your business logic here } catch (MyException ex) { txManager.rollback(status); throw ex; } txManager.commit(status); ``` Kotlin ``` val def = DefaultTransactionDefinition() // explicitly setting the transaction name is something that can be done only programmatically def.setName("SomeTxName") def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED val status = txManager.getTransaction(def) try { // put your business logic here } catch (ex: MyException) { txManager.rollback(status) throw ex } txManager.commit(status) ``` ##### 使用`ReactiveTransactionManager` 在处理反应性事务时,可以直接使用 `org.springframework.transaction.reactiveTransactionManager’来管理事务。要做到这一点,通过 Bean 引用将你使用的`ReactiveTransactionManager`的实现传递给你的 Bean。然后,通过使用`TransactionDefinition`和 `reactiveTransaction’对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点: Java ``` DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can be done only programmatically def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); Mono reactiveTx = txManager.getReactiveTransaction(def); reactiveTx.flatMap(status -> { Mono tx = ...; // put your business logic here return tx.then(txManager.commit(status)) .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex))); }); ``` Kotlin ``` val def = DefaultTransactionDefinition() // explicitly setting the transaction name is something that can be done only programmatically def.setName("SomeTxName") def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED val reactiveTx = txManager.getReactiveTransaction(def) reactiveTx.flatMap { status -> val tx = ... // put your business logic here tx.then(txManager.commit(status)) .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) } } ``` ### 1.6.在程序化事务管理和声明式事务管理之间进行选择 只有当你有少量的事务操作时,程序化事务管理通常才是一个好主意。例如,如果你有一个 Web 应用程序,该应用程序仅需要用于某些更新操作的事务,那么你可能不希望通过使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用“交易模板”可能是一种很好的方法。能够显式地设置事务名称也只能通过使用程序化的事务管理方法来完成。 另一方面,如果你的应用程序有许多事务操作,声明式事务管理通常是值得的。它使事务管理脱离了业务逻辑,并且不难配置。 Spring 当使用框架而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。 ### 1.7.事务绑定事件 根据 Spring 4.2,事件的侦听器可以绑定到事务的一个阶段。典型的示例是在事务成功完成时处理事件。当当前事务的结果对侦听器确实很重要时,这样做可以更灵活地使用事件。 你可以使用`@EventListener`注释来注册一个常规的事件侦听器。如果需要将其绑定到事务,请使用`@TransactionalEventListener`。这样做时,默认情况下侦听器将绑定到事务的提交阶段。 下一个示例展示了这个概念。假设组件发布了一个命令创建的事件,并且我们希望定义一个侦听器,该侦听器只应在成功提交了它所发布的事务之后才处理该事件。下面的示例设置了这样的事件侦听器: Java ``` @Component public class MyComponent { @TransactionalEventListener public void handleOrderCreatedEvent(CreationEvent creationEvent) { // ... } } ``` Kotlin ``` @Component class MyComponent { @TransactionalEventListener fun handleOrderCreatedEvent(creationEvent: CreationEvent) { // ... } } ``` `@TransactionalEventListener`注释公开了一个`phase`属性,该属性允许你定制侦听器应该绑定到的事务的阶段。有效的阶段是`BEFORE_COMMIT`,`AFTER_COMMIT`(默认),`AFTER_ROLLBACK`,以及汇总事务完成(无论是提交还是回滚)的 `after_complete’。 如果没有事务正在运行,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,你可以通过将注释的`fallbackExecution`属性设置为`true`来重写该行为。 | |`@TransactionalEventListener`仅适用于由“Platform TransactionManager”管理的线程绑定事务。由`ReactiveTransactionManager`管理的反应性事务使用反应器上下文而不是线程本地属性,因此从
事件侦听器的角度来看,它不能参与兼容的活动事务。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.8.特定于应用服务器的集成 Spring 的事务抽象通常与应用服务器无关。此外, Spring 的`JtaTransactionManager`类(它可以选择性地为 JTA`UserTransaction`和`TransactionManager`对象执行 JNDI 查找)自动检测后一个对象的位置,该位置因应用程序服务器的不同而不同。访问 JTA 的“TransactionManager”允许增强事务语义——特别是支持事务暂停。有关详细信息,请参见[“JTATRANSACTIONMANER”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/jta/JtaTransactionManager.html)Javadoc。 Spring 的`JtaTransactionManager`是在 Java EE 应用程序服务器上运行的标准选择,并且已知可在所有公共服务器上运行。高级功能(如事务挂起)也可以在许多服务器上运行(包括 GlassFish、JBoss 和 Geronimo),而不需要任何特殊配置。然而,对于完全支持的事务挂起和进一步的高级集成, Spring 包括用于 WebLogic Server 和 WebSphere 的特殊适配器。这些适配器将在下面的小节中进行讨论。 对于包括 WebLogic Server 和 WebSphere 在内的标准场景,可以考虑使用方便的``配置元素。在进行配置时,此元素会自动检测基础服务器,并为平台选择最佳的事务管理器。这意味着你不需要显式地配置特定于服务器的适配器类(如以下小节中讨论的那样)。相反,它们是自动选择的,标准的“jtaTransactionManager”是默认的后备选项。 #### 1.8.1.IBMWebSphere 在 WebSphere6.1.0.9 及以上版本中,推荐使用的 Spring JTA 事务管理器是“WebSphereOwTransactionManager”。这个特殊的适配器使用 IBM 的`UOWManager`API,它在 WebSphereApplicationServer6.1.0.9 及更高版本中可用。有了这个适配器, Spring 驱动的事务暂停(由 `pragation_requires_new’发起的暂停和恢复)得到了 IBM 的正式支持。 #### 1.8.2.Oracle WebLogic 服务器 在 WebLogic Server9.0 或更高版本中,你通常会使用“WebLogicJTatRansactionManager”,而不是 stock`JtaTransactionManager`类。普通`JtaTransactionManager`的这个特殊的特定于 WebLogic 的子类在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能,而不是标准的 JTA 语义。功能包括事务名称、每个事务隔离级别,以及在所有情况下正确地恢复事务。 ### 1.9.常见问题的解决方案 这一节描述了一些常见问题的解决方案。 #### 1.9.1.对特定的`DataSource`#### 使用错误的事务管理器 根据事务技术和需求的选择,使用正确的`PlatformTransactionManager`实现。 Spring 框架使用得当,仅提供了一种直接的、可移植的抽象。如果使用全局事务,则必须使用 `org.springframework.transaction.jta.jtatransactionmanager’类(或它的[特定于应用服务器的子类](#transaction-application-server-integration))执行所有事务操作。否则,事务基础结构将尝试在容器`DataSource`实例等资源上执行本地事务。这样的本地事务是没有意义的,一个好的应用程序服务器会将它们视为错误。 ### 1.10.更多资源 有关 Spring 框架的事务支持的更多信息,请参见: * [Distributed transactions in Spring, with and without XA](https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html)是一个 JavaWorld 演示文稿,其中 Spring 的 David Syer 指导你了解 Spring 应用程序中分布式事务的七种模式,其中三种有 XA,四种没有 XA。 * [*Java 事务设计策略 *](https://www.infoq.com/minibooks/JTDS)是一本可从[InfoQ](https://www.infoq.com/)获得的书,它提供了关于 Java 事务的快节奏的介绍。它还包括关于如何配置和使用 Spring 框架和 EJB3 的事务的并排示例。 ## 2. DAO 支持 Spring 中的数据访问对象支持旨在使其易于以一致的方式与数据访问技术(例如 JDBC、 Hibernate 或 JPA)一起工作。这使你可以相当容易地在上述持久性技术之间进行切换,并且还允许你编写代码,而无需担心捕获特定于每种技术的异常。 ### 2.1.一致的异常层次结构 Spring 提供了一种从技术特定的异常(例如 SQLException)到其自身的异常类层次结构的方便转换,其具有将`DataAccessException`作为根异常的特性。这些异常包装了原始异常,这样就不会有任何风险,你可能会丢失任何有关可能出了什么问题的信息。 除了 JDBC 异常之外, Spring 还可以包装 JPA 和 Hibernate 特定的异常,将它们转换为一组重点运行时异常。这样,你就可以只在适当的层中处理大多数不可恢复的持久性异常,而不需要在 DAO 中有烦人的样板抓取和抛出块和异常声明。正如上面提到的,JDBC 异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。 前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类。如果使用基于拦截器的类,则应用程序必须关心处理`HibernateExceptions`和`PersistenceExceptions`本身,最好是通过分别将`convertHibernateAccessException(..)`或`convertJpaAccessException(..)`方法委托给`SessionFactoryUtils`。这些方法将异常转换为与`org.springframework.dao`异常层次结构中的异常兼容的异常。由于`PersistenceExceptions`未被选中,它们也可能被抛出(尽管在异常方面牺牲了泛型 DAO 抽象)。 下面的图像显示了 Spring 提供的异常层次结构。(请注意,图片中详细介绍的类层次结构只显示了整个“DataAccessException”层次结构的一个子集。 ![DataAccessCeption](images/DataAccessException.png) ### 2.2.用于配置 DAO 或存储库类的注释 保证你的数据访问对象或存储库提供异常转换的最佳方法是使用`@Repository`注释。这种注释还允许组件扫描支持查找和配置你的 DAO 和存储库,而无需为它们提供 XML 配置条目。下面的示例展示了如何使用`@Repository`注释: Java ``` @Repository (1) public class SomeMovieFinder implements MovieFinder { // ... } ``` |**1**|`@Repository`注释。| |-----|-----------------------------| Kotlin ``` @Repository (1) class SomeMovieFinder : MovieFinder { // ... } ``` |**1**|`@Repository`注释。| |-----|-----------------------------| 任何 DAO 或存储库实现都需要访问持久性资源,这取决于所使用的持久性技术。例如,基于 JDBC 的存储库需要访问 JDBC`DataSource`,而基于 JPA 的存储库需要访问 `EntityManager’。实现这一点的最简单方法是通过使用`@Autowired`、`@Inject`、`@Resource`或`@PersistenceContext`注释中的一个来注入这种资源依赖关系。下面的示例适用于 JPA 存储库: Java ``` @Repository public class JpaMovieFinder implements MovieFinder { @PersistenceContext private EntityManager entityManager; // ... } ``` Kotlin ``` @Repository class JpaMovieFinder : MovieFinder { @PersistenceContext private lateinit var entityManager: EntityManager // ... } ``` 如果使用经典的 Hibernate API,则可以插入`SessionFactory`,如下例所示: Java ``` @Repository public class HibernateMovieFinder implements MovieFinder { private SessionFactory sessionFactory; @Autowired public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } // ... } ``` Kotlin ``` @Repository class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder { // ... } ``` 我们在这里展示的最后一个示例是典型的 JDBC 支持。你可以将“DataSource”注入到初始化方法或构造函数中,通过使用`DataSource`,可以在其中创建“JDBcTemplate”和其他数据访问支持类(例如`SimpleJdbcCall`和其他类)。以下示例自动连接`DataSource`: Java ``` @Repository public class JdbcMovieFinder implements MovieFinder { private JdbcTemplate jdbcTemplate; @Autowired public void init(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } // ... } ``` Kotlin ``` @Repository class JdbcMovieFinder(dataSource: DataSource) : MovieFinder { private val jdbcTemplate = JdbcTemplate(dataSource) // ... } ``` | |有关如何
配置应用程序上下文以利用这些注释的详细信息,请参见每个持久性技术的具体覆盖范围。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------| ## 3. 使用 JDBC 进行数据访问 Spring 框架 JDBC 抽象所提供的值可能最好通过下表中概述的操作序列来显示。该表显示了 Spring 处理哪些行为,以及哪些行为是你的责任。 |行动|Spring|You| |--------------------------------------------------------|------|---| |定义连接参数。| | X | |打开连接。| X | | |指定 SQL 语句。| | X | |声明参数并提供参数值| | X | |准备并运行该语句。| X | | |设置循环以遍历结果(如果有的话)。| X | | |完成每个迭代的工作。| | X | |处理任何异常。| X | | |处理交易。| X | | |关闭连接、语句和结果集。| X | | Spring 框架负责处理所有可能使 JDBC 成为如此乏味的 API 的底层细节。 ### 3.1.一种 JDBC 数据库访问方法的选择 你可以在几种方法中进行选择,以形成你的 JDBC 数据库访问的基础。除了三种类型的`JdbcTemplate`之外,新的`SimpleJdbcInsert`和“SimpleJDBCCall”方法优化了数据库元数据,并且 RDBMS 对象风格采用了一种类似于 JDO 查询设计的更面向对象的方法。一旦你开始使用这些方法之一,你仍然可以进行混合和匹配,以包含来自不同方法的功能。所有的方法都需要一个符合 JDBC2.0 的驱动程序,而一些高级特性则需要一个 JDBC3.0 驱动程序。 * `JdbcTemplate`是经典且最流行的 Spring JDBC 方法。这种“最底层”的方法和所有其他方法都使用了 JDBCtemplate。 * `NamedParameterJdbcTemplate`封装一个`JdbcTemplate`以提供命名参数,而不是传统的 JDBC`?`占位符。当你对 SQL 语句有多个参数时,这种方法提供了更好的文档和易用性。 * `SimpleJdbcInsert`和`SimpleJdbcCall`优化数据库元数据,以限制所需配置的数量。这种方法简化了编码,因此只需要提供表或过程的名称,并提供匹配列名称的参数映射。只有当数据库提供了足够的元数据时,这种方法才会起作用。如果数据库不提供此元数据,则必须提供参数的显式配置。 * RDBMS 对象——包括`MappingSqlQuery`、`SqlUpdate`和`StoredProcedure`——要求你在数据访问层的初始化过程中创建可重用和线程安全的对象。这种方法是仿照 JDO 查询建模的,在 JDO 查询中,你定义查询字符串、声明参数并编译查询。一旦这样做了,`execute(…)`、`update(…​)`和`findObject(…​)`方法就可以用各种参数值多次调用。 ### 3.2.包层次结构 Spring 框架的 JDBC 抽象框架由四个不同的包组成: * `core`:`org.springframework.jdbc.core`包包含`JdbcTemplate`类及其各种回调接口,以及各种相关的类。名为 `org.springframework.jdbc.core.simple’的子包包含`SimpleJdbcInsert`和 `SimpleJDBCCall’类。另一个名为 `org.springframework.jdbc.core.namedparam’的子包包含`NamedParameterJdbcTemplate`类和相关的支持类。见[使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理](#jdbc-core),[JDBC 批处理操作](#jdbc-advanced-jdbc),和[Simplifying JDBC Operations with the `SimpleJdbc` Classes](#jdbc-simple-jdbc)。 * `datasource`:`org.springframework.jdbc.datasource`包包含一个用于轻松访问 ` 数据源’的实用程序类和各种简单的`DataSource`实现,你可以使用这些实现在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。一个名为`org.springfamework.jdbc.datasource.embedded`的子包提供了通过使用 Java 数据库引擎创建嵌入式数据库的支持,例如 HSQL、H2 和 Derby。见[控制数据库连接](#jdbc-connections)和[嵌入式数据库支持](#jdbc-embedded-database-support)。 * `object`:`org.springframework.jdbc.object`包包含表示 RDBMS 查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。见[将 JDBC 操作建模为 Java 对象](#jdbc-object)。这种方法是由 JDO 建模的,尽管查询返回的对象自然地与数据库断开连接。这个较高级别的 JDBC 抽象依赖于`org.springframework.jdbc.core`包中的较低级别抽象。 * `support`:`org.springframework.jdbc.support`包提供了`SQLException`翻译功能和一些实用程序类。在 JDBC 处理过程中抛出的异常被转换为`org.springframework.dao`包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换后的异常都未被选中,这使你可以选择捕获异常,以便在允许将其他异常传播给调用方的同时恢复这些异常。见[Using `SQLExceptionTranslator`](#jdbc-SQLExceptionTranslator)。 ### 3.3.使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理 本节介绍如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理。它包括以下主题: * [Using `JdbcTemplate`](#jdbc-JdbcTemplate) * [Using `NamedParameterJdbcTemplate`](#jdbc-NamedParameterJdbcTemplate) * [Using `SQLExceptionTranslator`](#jdbc-SQLExceptionTranslator) * [正在运行的语句](#jdbc-statements-executing) * [Running Queries](#jdbc-statements-querying) * [更新数据库](#jdbc-updates) * [检索自动生成的密钥](#jdbc-auto-generated-keys) #### 3.3.1.使用`JdbcTemplate` `JdbcTemplate`是 JDBC 核心包中的中心类。它处理资源的创建和发布,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(例如语句的创建和执行),留下应用程序代码来提供 SQL 和提取结果。`JdbcTemplate`类: * 运行 SQL 查询 * 更新语句和存储过程调用 * 在`ResultSet`实例上执行迭代,并提取返回的参数值。 * 捕获 JDBC 异常,并将它们转换为在`org.springframework.dao`包中定义的通用的、信息量更大的异常层次结构。(见[一致的异常层次结构](#dao-exceptions)。 当你为代码使用`JdbcTemplate`时,你只需要实现回调接口,并为它们提供一个明确定义的契约。给定一个由 `JDBcTemplate’类提供的`Connection`,`PreparedStatementCreator`回调接口将创建一个准备好的语句,提供 SQL 和任何必要的参数。“CallableStatementCreator”接口也是如此,它创建了可调用语句。“rowcallbackhandler”接口从`ResultSet`的每一行提取值。 你可以通过使用`DataSource`引用的直接实例化在 DAO 实现中使用`JdbcTemplate`,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。 | |`DataSource`应该始终配置为 Spring IOC 容器中的 Bean。在
中,第一种情况将 Bean 直接赋予服务;在第二种情况下,将
赋予准备好的模板。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 这个类发布的所有 SQL 都记录在`DEBUG`级别的目录下,该目录对应于模板实例的完全限定类名称(通常是 `jdbctemplate’,但如果使用 `jdbctemplate’类的自定义子类,则可能会有所不同)。 下面的部分提供了`JdbcTemplate`用法的一些示例。这些示例并不是`JdbcTemplate`公开的所有功能的详尽列表。关于这一点,请参见乘务员[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html)。 ##### 查询(“选择”) 下面的查询获取关系中的行数: Java ``` int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class); ``` Kotlin ``` val rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor")!! ``` 以下查询使用了一个绑定变量: Java ``` int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", Integer.class, "Joe"); ``` Kotlin ``` val countOfActorsNamedJoe = jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!! ``` 下面的查询查找`String`: Java ``` String lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", String.class, 1212L); ``` Kotlin ``` val lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", arrayOf(1212L))!! ``` 下面的查询查找并填充一个域对象: Java ``` Actor actor = jdbcTemplate.queryForObject( "select first_name, last_name from t_actor where id = ?", (resultSet, rowNum) -> { Actor newActor = new Actor(); newActor.setFirstName(resultSet.getString("first_name")); newActor.setLastName(resultSet.getString("last_name")); return newActor; }, 1212L); ``` Kotlin ``` val actor = jdbcTemplate.queryForObject( "select first_name, last_name from t_actor where id = ?", arrayOf(1212L)) { rs, _ -> Actor(rs.getString("first_name"), rs.getString("last_name")) } ``` 以下查询查找并填充域对象列表: Java ``` List actors = this.jdbcTemplate.query( "select first_name, last_name from t_actor", (resultSet, rowNum) -> { Actor actor = new Actor(); actor.setFirstName(resultSet.getString("first_name")); actor.setLastName(resultSet.getString("last_name")); return actor; }); ``` Kotlin ``` val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ -> Actor(rs.getString("first_name"), rs.getString("last_name")) ``` 如果最后两个代码片段实际上存在于同一个应用程序中,那么删除两个`RowMapper`lambda 表达式中存在的重复并将它们提取到一个字段中是有意义的,然后可以根据需要由 DAO 方法引用。例如,将前面的代码片段编写如下可能更好: Java ``` private final RowMapper actorRowMapper = (resultSet, rowNum) -> { Actor actor = new Actor(); actor.setFirstName(resultSet.getString("first_name")); actor.setLastName(resultSet.getString("last_name")); return actor; }; public List findAllActors() { return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper); } ``` Kotlin ``` val actorMapper = RowMapper { rs: ResultSet, rowNum: Int -> Actor(rs.getString("first_name"), rs.getString("last_name")) } fun findAllActors(): List { return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper) } ``` ##### 用`JdbcTemplate`更新(` 插入 `,`UPDATE`,和`DELETE`) 可以使用`update(..)`方法执行插入、更新和删除操作。参数值通常以变量参数或对象数组的形式提供。 下面的示例插入一个新条目: Java ``` this.jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling"); ``` Kotlin ``` jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling") ``` 下面的示例更新了一个现有条目: Java ``` this.jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", "Banjo", 5276L); ``` Kotlin ``` jdbcTemplate.update( "update t_actor set last_name = ? where id = ?", "Banjo", 5276L) ``` 下面的示例删除一个条目: Java ``` this.jdbcTemplate.update( "delete from t_actor where id = ?", Long.valueOf(actorId)); ``` Kotlin ``` jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong()) ``` ##### 其它`JdbcTemplate`操作 你可以使用`execute(..)`方法来运行任意 SQL。因此,该方法通常用于 DDL 语句。它被接受回调接口、绑定变量数组等的变量严重超载。下面的示例创建了一个表: Java ``` this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); ``` Kotlin ``` jdbcTemplate.execute("create table mytable (id integer, name varchar(100))") ``` 下面的示例调用一个存储过程: 爪哇 ``` this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId)); ``` Kotlin ``` jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", unionId.toLong()) ``` 更复杂的存储过程支持是[covered later](#jdbc-StoredProcedure)。 ##### `JdbcTemplate`最佳实践 一旦配置好,`JdbcTemplate`类的实例是线程安全的。这很重要,因为这意味着你可以配置`JdbcTemplate`的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。`JdbcTemplate`是有状态的,因为它保持了对`DataSource`的引用,但是这个状态不是会话状态。 在使用`JdbcTemplate`类(以及相关的[NamedParameterJDBcTemplate](#jdbc-NamedParameterJdbcTemplate)类)时,一种常见的做法是在 Spring 配置文件中配置`DataSource`,然后在依赖项-将共享的`DataSource` Bean 注入到 DAO 类中。在 setter 中为`DataSource`创建了`JdbcTemplate`。这导致了类似于以下的 DAO: 爪哇 ``` public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } // JDBC-backed implementations of the methods on the CorporateEventDao follow... } ``` Kotlin ``` class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { private val jdbcTemplate = JdbcTemplate(dataSource) // JDBC-backed implementations of the methods on the CorporateEventDao follow... } ``` 下面的示例展示了相应的 XML 配置: ``` ``` 显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以用`@Repository`注释类(这使它成为组件扫描的候选),并用`DataSource`setter 方法注解`@Autowired`。下面的示例展示了如何做到这一点: 爪哇 ``` @Repository (1) public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; @Autowired (2) public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); (3) } // JDBC-backed implementations of the methods on the CorporateEventDao follow... } ``` |**1**|用`@Repository`注释类。| |-----|----------------------------------------------------------| |**2**|用`@Autowired`注释`DataSource`setter 方法。| |**3**|用`DataSource`创建一个新的`JdbcTemplate`。| Kotlin ``` @Repository (1) class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2) private val jdbcTemplate = JdbcTemplate(dataSource) (3) // JDBC-backed implementations of the methods on the CorporateEventDao follow... } ``` |**1**|用`@Repository`注释类。| |-----|--------------------------------------------------| |**2**|`DataSource`的构造函数注入。| |**3**|用`DataSource`创建一个新的`JdbcTemplate`。| 下面的示例展示了相应的 XML 配置: ``` ``` 如果你使用 Spring 的`JdbcDaoSupport`类,并且你的各种支持 JDBC 的 DAO 类从中扩展,那么你的子类将从 `JDBCDAOSupport’类继承一个`setDataSource(..)`方法。你可以选择是否从这个类继承。提供“JDBCDAOSupport”类只是为了方便。 无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建`JdbcTemplate`类的新实例。一旦配置好,`JdbcTemplate`实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个`JdbcTemplate`实例,这需要多个`DataSources`实例,然后需要多个配置不同的`JdbcTemplate`实例。 #### 3.3.2.使用`NamedParameterJdbcTemplate` `NamedParameterJdbcTemplate`类增加了对使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符(`'?'`)参数编程 JDBC 语句。`NamedParameterJdbcTemplate`类包装了一个 `JDBcTemplate’,并将其委托给包装好的`JdbcTemplate`,以完成其大部分工作。本节仅描述`NamedParameterJdbcTemplate`类中与`JdbcTemplate`本身不同的区域——即通过使用命名参数编程 JDBC 语句。下面的示例展示了如何使用`NamedParameterJdbcTemplate`: 爪哇 ``` // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ``` Kotlin ``` private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun countOfActorsByFirstName(firstName: String): Int { val sql = "select count(*) from T_ACTOR where first_name = :first_name" val namedParameters = MapSqlParameterSource("first_name", firstName) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } ``` 请注意,在分配给`sql`变量的值和插入`namedParameters`变量(类型`MapSqlParameterSource`)的对应值中使用了命名参数表示法。 或者,你可以使用基于`Map`的样式,将命名参数及其对应值传递给 `NamedParameterJDbcTemplate’实例。由`NamedParameterJdbcOperations`公开并由 `NamedParameterJDBcTemplate’类实现的其余方法遵循类似的模式,在此不涉及。 下面的示例展示了基于`Map`的样式的使用: 爪哇 ``` // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; Map namedParameters = Collections.singletonMap("first_name", firstName); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ``` Kotlin ``` // some JDBC-backed DAO class... private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun countOfActorsByFirstName(firstName: String): Int { val sql = "select count(*) from T_ACTOR where first_name = :first_name" val namedParameters = mapOf("first_name" to firstName) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } ``` 与`NamedParameterJdbcTemplate`(存在于同一个 爪哇 包中)相关的一个不错的特性是`SqlParameterSource`接口。你已经在前面的代码片段(“mapsqlparameterSource”类)中看到了这个接口实现的示例。`SqlParameterSource`是`NamedParameterJdbcTemplate`的命名参数值的来源。`MapSqlParameterSource`类是一个简单的实现,它是一个围绕`java.util.Map`的适配器,其中键是参数名称,值是参数值。 另一个`SqlParameterSource`实现是`BeanPropertySqlParameterSource`类。这个类包装一个任意的 爪哇Bean(即一个坚持[爪哇Bean 公约](https://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html)的类的实例),并使用包装好的 爪哇Bean 的属性作为命名参数值的源。 下面的示例展示了一个典型的 爪哇Bean: 爪哇 ``` public class Actor { private Long id; private String firstName; private String lastName; public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public Long getId() { return this.id; } // setters omitted... } ``` Kotlin ``` data class Actor(val id: Long, val firstName: String, val lastName: String) ``` 下面的示例使用`NamedParameterJdbcTemplate`返回前面示例中所示的类的成员数: 爪哇 ``` // some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActors(Actor exampleActor) { // notice how the named parameters match the properties of the above 'Actor' class String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); } ``` Kotlin ``` // some JDBC-backed DAO class... private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun countOfActors(exampleActor: Actor): Int { // notice how the named parameters match the properties of the above 'Actor' class val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName" val namedParameters = BeanPropertySqlParameterSource(exampleActor) return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! } ``` 请记住,`NamedParameterJdbcTemplate`类封装了一个经典的`JdbcTemplate`模板。如果需要访问打包的`JdbcTemplate`实例以访问仅在`JdbcTemplate`类中存在的功能,则可以使用 `getjdbcoperations()’方法通过 `jdbcoperations’接口访问打包的`JdbcTemplate`。 另请参见[“JDBcTemplate”最佳做法](#jdbc-JdbcTemplate-idioms)关于在应用程序上下文中使用 `NamedParameterJDBcTemplate’类的指导方针。 #### 3.3.3.使用`SQLExceptionTranslator` `SQLExceptionTranslator`是一个由类实现的接口,它可以在`SQLException`s 和 Spring 自己的`org.springframework.dao.DataAccessException`之间转换,这在数据访问策略方面是不可知的。实现可以是通用的(例如,为 JDBC 使用 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码),以获得更高的精度。 `SQLErrorCodeSQLExceptionTranslator`是默认情况下使用的`SQLExceptionTranslator`的实现。该实现使用特定的供应商代码。它比`SQLState`实现更精确。错误代码转换是基于在一个名为`SQLErrorCodes`的 爪哇Bean 类型类中保存的代码。这个类由`SQLErrorCodesFactory`创建和填充,它(顾名思义)是一个工厂,用于基于名为 `sql-error-codes.xml’的配置文件的内容创建`SQLErrorCodes`。该文件填充了供应商代码,并基于`DatabaseMetaData`中的 `DatabaseProductName’。使用了你正在使用的实际数据库的代码。 `SQLErrorCodeSQLExceptionTranslator`按以下顺序应用匹配规则: 1. 由子类实现的任何自定义转换。通常情况下,使用提供的具体“sqlerrorcodesqlexceptiontranslator”,因此此规则不适用。它仅在你实际提供了一个子类实现的情况下才适用。 2. 作为`SQLErrorCodes`类的`customSqlExceptionTranslator`属性提供的`SQLExceptionTranslator`接口的任何自定义实现。 3. 搜索`CustomSQLErrorCodesTranslation`类的实例列表(为`SQLErrorCodes`类的 `CustomTranslations’属性提供)以查找匹配项。 4. 应用了错误码匹配。 5. 使用后备翻译程序。`SQLExceptionSubclassTranslator`是默认的后备翻译程序。如果此翻译不可用,则下一个后备翻译程序是`SQLStateSQLExceptionTranslator`。 | |默认情况下,`SQLErrorCodesFactory`用于定义`Error`代码和自定义异常
翻译。它们是在一个名为`sql-error-codes.xml`的文件中从
Classpath 中查找的,并且匹配的`SQLErrorCodes`实例是位于基于数据库
名称的数据库中使用的数据库元数据。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以扩展`SQLErrorCodeSQLExceptionTranslator`,如下例所示: 爪哇 ``` public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) { if (sqlEx.getErrorCode() == -12345) { return new DeadlockLoserDataAccessException(task, sqlEx); } return null; } } ``` Kotlin ``` class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() { override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? { if (sqlEx.errorCode == -12345) { return DeadlockLoserDataAccessException(task, sqlEx) } return null } } ``` 在前面的示例中,将翻译特定的错误代码(“-12345”),而其他错误将由默认的翻译实现来翻译。要使用此自定义转换器,你必须通过方法“setExceptionTranslator”将其传递给`JdbcTemplate`,并且你必须在需要此转换器的所有数据访问处理中使用此`JdbcTemplate`。下面的示例展示了如何使用这个自定义转换器: 爪哇 ``` private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { // create a JdbcTemplate and set data source this.jdbcTemplate = new JdbcTemplate(); this.jdbcTemplate.setDataSource(dataSource); // create a custom translator and set the DataSource for the default translation lookup CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); tr.setDataSource(dataSource); this.jdbcTemplate.setExceptionTranslator(tr); } public void updateShippingCharge(long orderId, long pct) { // use the prepared JdbcTemplate for this update this.jdbcTemplate.update("update orders" + " set shipping_charge = shipping_charge * ? / 100" + " where id = ?", pct, orderId); } ``` Kotlin ``` // create a JdbcTemplate and set data source private val jdbcTemplate = JdbcTemplate(dataSource).apply { // create a custom translator and set the DataSource for the default translation lookup exceptionTranslator = CustomSQLErrorCodesTranslator().apply { this.dataSource = dataSource } } fun updateShippingCharge(orderId: Long, pct: Long) { // use the prepared JdbcTemplate for this update this.jdbcTemplate!!.update("update orders" + " set shipping_charge = shipping_charge * ? / 100" + " where id = ?", pct, orderId) } ``` 自定义翻译器被传递给一个数据源,以便在“sql-error-codes.xml”中查找错误代码。 #### 3.3.4.正在运行的语句 运行 SQL 语句只需要很少的代码。你需要`DataSource`和 `JDBcTemplate’,包括 `JDBcTemplate’提供的方便方法。下面的示例展示了创建一个新表的最小但功能齐全的类需要包括哪些内容: 爪哇 ``` import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAStatement { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void doExecute() { this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); } } ``` Kotlin ``` import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate class ExecuteAStatement(dataSource: DataSource) { private val jdbcTemplate = JdbcTemplate(dataSource) fun doExecute() { jdbcTemplate.execute("create table mytable (id integer, name varchar(100))") } } ``` #### 3.3.5.正在运行的查询 一些查询方法返回一个值。要从一行中检索计数或特定值,请使用`queryForObject(..)`。后者将返回的 JDBC`Type`转换为作为参数传入的 爪哇 类。如果类型转换无效,将抛出“InvalidDataAccessiusAgeException”。下面的示例包含两个查询方法,一个用于查询`int`,另一个用于查询`String`: 爪哇 ``` import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class RunAQuery { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int getCount() { return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class); } public String getName() { return this.jdbcTemplate.queryForObject("select name from mytable", String.class); } } ``` Kotlin ``` import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate class RunAQuery(dataSource: DataSource) { private val jdbcTemplate = JdbcTemplate(dataSource) val count: Int get() = jdbcTemplate.queryForObject("select count(*) from mytable")!! val name: String? get() = jdbcTemplate.queryForObject("select name from mytable") } ``` 除了单个结果查询方法外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是`queryForList(..)`,它返回一个`List`,其中每个元素都是`Map`,包含每个列的一个条目,并使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,它可能如下: 爪哇 ``` private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public List> getList() { return this.jdbcTemplate.queryForList("select * from mytable"); } ``` Kotlin ``` private val jdbcTemplate = JdbcTemplate(dataSource) fun getList(): List> { return jdbcTemplate.queryForList("select * from mytable") } ``` 返回的列表类似于以下内容: ``` [{name=Bob, id=1}, {name=Mary, id=2}] ``` #### 3.3.6.更新数据库 下面的示例更新了某个主键的列: 爪哇 ``` import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void setName(int id, String name) { this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id); } } ``` Kotlin ``` import javax.sql.DataSource import org.springframework.jdbc.core.JdbcTemplate class ExecuteAnUpdate(dataSource: DataSource) { private val jdbcTemplate = JdbcTemplate(dataSource) fun setName(id: Int, name: String) { jdbcTemplate.update("update mytable set name = ? where id = ?", name, id) } } ``` 在前面的示例中,SQL 语句具有行参数的占位符。你可以将参数值以 varargs 的形式传递,也可以以对象数组的形式传递。因此,你应该在原语包装器类中显式地包装原语,或者应该使用自动装箱。 #### 3.3.7.检索自动生成的密钥 一种`update()`方便的方法支持对数据库生成的主键进行检索。这种支持是 JDBC3.0 标准的一部分。有关详细信息,请参见说明书第 13.6 章。该方法以`PreparedStatementCreator`作为其第一个参数,这就是指定所需 INSERT 语句的方式。另一个参数是`KeyHolder`,它包含更新成功返回时生成的键。没有标准的单一方法来创建适当的`PreparedStatement`(这解释了为什么方法签名是这样的)。以下示例可以在 Oracle 上运行,但可能无法在其他平台上运行: 爪哇 ``` final String INSERT_SQL = "insert into my_test (name) values(?)"; final String name = "Rob"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" }); ps.setString(1, name); return ps; }, keyHolder); // keyHolder.getKey() now contains the generated key ``` Kotlin ``` val INSERT_SQL = "insert into my_test (name) values(?)" val name = "Rob" val keyHolder = GeneratedKeyHolder() jdbcTemplate.update({ it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) } }, keyHolder) // keyHolder.getKey() now contains the generated key ``` ### 3.4.控制数据库连接 本节内容包括: * [Using `DataSource`](#jdbc-datasource) * [Using `DataSourceUtils`](#jdbc-DataSourceUtils) * [Implementing `SmartDataSource`](#jdbc-SmartDataSource) * [Extending `AbstractDataSource`](#jdbc-AbstractDataSource) * [Using `SingleConnectionDataSource`](#jdbc-SingleConnectionDataSource) * [Using `DriverManagerDataSource`](#jdbc-DriverManagerDataSource) * [Using `TransactionAwareDataSourceProxy`](#jdbc-TransactionAwareDataSourceProxy) * [Using `DataSourceTransactionManager`](#jdbc-DataSourceTransactionManager) #### 3.4.1.使用`DataSource` Spring 通过`DataSource`获得到数据库的连接。a`DataSource`是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。 当你使用 Spring 的 JDBC 层时,你可以从 JNDI 获得数据源,也可以使用第三方提供的连接池实现来配置你自己的数据源。传统的选择是带有 Bean 风格`DataSource`类的 ApacheCommonsDBCP 和 C3P0;对于现代的 JDBC 连接池,请考虑使用其 Builder 风格的 API 的 Hikaricp。 | |你应该使用`DriverManagerDataSource`和`SimpleDriverDataSource`类
(包含在 Spring 发行版中)仅用于测试目的!这些变体不
提供池,并且在发出多个连接请求时性能很差。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的部分使用 Spring 的`DriverManagerDataSource`实现。其他几个`DataSource`变体将在后面介绍。 要配置`DriverManagerDataSource`: 1. 获得与`DriverManagerDataSource`的连接,就像你通常获得 JDBC 连接一样。 2. 指定 JDBC 驱动程序的完全限定类名,以便`DriverManager`可以加载驱动程序类。 3. 提供不同于 JDBC 驱动程序的 URL。(请参阅驱动程序的文档以获得正确的值。 4. 提供连接到数据库的用户名和密码。 下面的示例展示了如何在 爪哇 中配置`DriverManagerDataSource`: 爪哇 ``` DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword(""); ``` Kotlin ``` val dataSource = DriverManagerDataSource().apply { setDriverClassName("org.hsqldb.jdbcDriver") url = "jdbc:hsqldb:hsql://localhost:" username = "sa" password = "" } ``` 下面的示例展示了相应的 XML 配置: ``` ``` 接下来的两个示例展示了 DBCP 和 C3P0 的基本连接和配置。要了解有助于控制池特性的更多选项,请参阅相应的连接池实现的产品文档。 下面的示例展示了 DBCP 配置: ``` ``` 下面的示例展示了 C3P0 配置: ``` ``` #### 3.4.2.使用`DataSourceUtils` `DataSourceUtils`类是一个方便且功能强大的助手类,它提供 ` 静态’方法来从 JNDI 获得连接,并在必要时关闭连接。它支持线程绑定连接,例如,`DataSourceTransactionManager`。 #### 3.4.3.实现`SmartDataSource` `SmartDataSource`接口应该由能够提供到关系数据库的连接的类来实现。它扩展了`DataSource`接口,让使用它的类查询在给定的操作之后是否应该关闭连接。当你知道需要重用某个连接时,这种用法是有效的。 #### 3.4.4.扩展`AbstractDataSource` `AbstractDataSource`是 Spring 的`abstract`实现的一个`DataSource`基类。它实现了所有`DataSource`实现所共有的代码。如果你编写自己的`DataSource`实现,则应该扩展`AbstractDataSource`类。 #### 3.4.5.使用`SingleConnectionDataSource` `SingleConnectionDataSource`类是`SmartDataSource`接口的实现,该接口封装单个`Connection`,该接口在每次使用后都不会关闭。这不是多线程能力。 如果任何客户机代码在假定池连接的情况下调用`close`(如使用持久性工具时),则应将`Map`属性设置为`true`。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为原生 Oracle`Connection`或类似的对象。 `SingleConnectionDataSource`主要是一个测试类。它通常支持在应用程序服务器之外,结合简单的 JNDI 环境,对代码进行简单的测试。与`DriverManagerDataSource`相反,它始终重用相同的连接,避免了过多地创建物理连接。 #### 3.4.6.使用`DriverManagerDataSource` `DriverManagerDataSource`类是标准`DataSource`接口的一种实现,该接口通过 Bean 属性配置一个普通的 JDBC 驱动程序,并每次返回一个新的 ` 连接’。 这种实现对于 爪哇 EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的`DataSource` Bean,也可以与简单的 JNDI 环境结合使用。池-假设`Connection.close()`调用关闭连接,那么任何`DataSource`可感知的持久性代码都应该工作。然而,即使在测试环境中,使用 爪哇Bean 风格的连接池(例如`commons-dbcp`)也非常容易,因此在 `DriverManagerDataSource’上使用这样的连接池几乎总是更好。 #### 3.4.7.使用`TransactionAwareDataSourceProxy` `TransactionAwareDataSourceProxy`是目标`DataSource`的代理。代理封装了目标`DataSource`,以添加对 Spring 管理的事务的感知。在这方面,它类似于事务 JNDI`DataSource`,由 爪哇 EE 服务器提供。 | |很少希望使用这个类,除非已经存在的代码必须被
调用并通过标准的 JDBC`DataSource`接口实现。在这种情况下,
仍然可以使该代码可用,同时,使该代码
参与 Spring 托管事务。通常情况下,最好使用用于资源管理的高级抽象来编写你的
自己的新代码,例如 `jdbctemplate’或`DataSourceUtils`。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 有关更多详细信息,请参见[“TransactionAwaredataSourceProxy”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.html)爪哇doc。 #### 3.4.8.使用`DataSourceTransactionManager` `DataSourceTransactionManager`类是单个 JDBC 数据源的`PlatformTransactionManager`实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,可能允许每个数据源有一个线程连接。 应用程序代码需要通过“datasourceutils.getConnection(数据源)”而不是 爪哇 EE 的标准“datasource.getConnection”来检索 JDBC 连接。它抛出未选中的`org.springframework.dao`异常,而不是选中的`LobHandler`。所有框架类(如`JdbcTemplate`)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。 `DataSourceTransactionManager`类支持自定义隔离级别和超时,这些级别和超时是根据适当的 JDBC 语句查询超时应用的。为了支持后者,应用程序代码必须使用`JdbcTemplate`,或者为每个创建的语句调用 `datasourceutils.applyTransactionTimeout’方法。 在单资源情况下,你可以使用这个实现,而不是`JtaTransactionManager`,因为它不需要容器支持 JTA。在两者之间切换只是一个配置问题,只要你坚持所需的连接查找模式。JTA 不支持自定义隔离级别。 ### 3.5.JDBC 批处理操作 如果对同一条准备好的语句进行多个调用的批处理,大多数 JDBC 驱动程序都会提供更好的性能。通过将更新分组为批,可以限制往返数据库的次数。 #### 3.5.1.带有`JdbcTemplate`的基本批处理操作 通过实现特殊接口的两种方法`JdbcTemplate`,并将该实现作为`batchUpdate`方法调用中的第二个参数传入,可以完成`JdbcTemplate`批处理。你可以使用`getBatchSize`方法来提供当前批处理的大小。你可以使用`setValues`方法来设置 prepared 语句的参数的值。这个方法被称为在`getBatchSize`调用中指定的次数。下面的示例基于列表中的条目更新`t_actor`表,并将整个列表用作批处理: 爪哇 ``` public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[] batchUpdate(final List actors) { return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", new BatchPreparedStatementSetter() { public void setValues(PreparedStatement ps, int i) throws SQLException { Actor actor = actors.get(i); ps.setString(1, actor.getFirstName()); ps.setString(2, actor.getLastName()); ps.setLong(3, actor.getId().longValue()); } public int getBatchSize() { return actors.size(); } }); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val jdbcTemplate = JdbcTemplate(dataSource) fun batchUpdate(actors: List): IntArray { return jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", object: BatchPreparedStatementSetter { override fun setValues(ps: PreparedStatement, i: Int) { ps.setString(1, actors[i].firstName) ps.setString(2, actors[i].lastName) ps.setLong(3, actors[i].id) } override fun getBatchSize() = actors.size }) } // ... additional methods } ``` 如果你处理一个更新流或从一个文件中读取数据,那么你可能有一个首选批处理大小,但是最后一个批处理可能没有那么多条目。在这种情况下,你可以使用`InterruptibleBatchPreparedStatementSetter`接口,它允许你在输入源耗尽时中断批处理。`isBatchExhausted`方法允许你发出批处理结束的信号。 #### 3.5.2.具有对象列表的批处理操作 `JdbcTemplate`和`NamedParameterJdbcTemplate`都提供了一种提供批更新的替代方式。你不是实现一个特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。框架在这些值上循环,并使用内部准备的语句设置器。API 会有所不同,这取决于你是否使用命名参数。对于已命名的参数,你提供一个“SQLParameterSource”数组,这是批处理的每个成员的一个条目。你可以使用 `SQLParameterSourceUtils.CreateBatch’便利方法来创建这个数组,传入一个由 Bean 样式的对象组成的数组(getter 方法对应于参数)、`string’-keyed`Map`实例(包含相应的参数作为值),或者两者的混合。 下面的示例显示了使用命名参数的批处理更新: 爪哇 ``` public class JdbcActorDao implements ActorDao { private NamedParameterTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int[] batchUpdate(List actors) { return this.namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", SqlParameterSourceUtils.createBatch(actors)); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) fun batchUpdate(actors: List): IntArray { return this.namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", SqlParameterSourceUtils.createBatch(actors)); } // ... additional methods } ``` 对于使用经典`?`占位符的 SQL 语句,你将传入一个包含带更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每个占位符有一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。 下面的示例与前面的示例相同,只是使用了经典的 JDBC`?`占位符: 爪哇 ``` public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[] batchUpdate(final List actors) { List batch = new ArrayList(); for (Actor actor : actors) { Object[] values = new Object[] { actor.getFirstName(), actor.getLastName(), actor.getId()}; batch.add(values); } return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val jdbcTemplate = JdbcTemplate(dataSource) fun batchUpdate(actors: List): IntArray { val batch = mutableListOf>() for (actor in actors) { batch.add(arrayOf(actor.firstName, actor.lastName, actor.id)) } return jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch) } // ... additional methods } ``` 我们前面描述的所有批更新方法都返回一个`int`数组,该数组包含每个批处理条目的受影响行数。这个计数是由 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个`-2`的值。 | |在这样的场景中,在底层`PreparedStatement`上自动设定值,
每个值对应的 JDBC 类型需要从给定的 爪哇 类型派生。
虽然这通常工作得很好,但可能会出现问题(例如,对于包含 map 的 `null’值)。 Spring,默认情况下,在这样的
情况下调用
,这在使用你的 JDBC 驱动程序时可能是昂贵的。你应该使用最近的驱动程序
版本,并考虑将`spring.jdbc.getParameterType.ignore`属性设置为`true`(作为 JVM 系统属性或通过`out`机制),如果你遇到
性能问题(如 Oracle12c、JBoss 和 PostgreSQL 上报告的那样)。

或者,你可以考虑通过`BatchPreparedStatementSetter`(如前面所示),通过显式类型
给出一个基于`List`的调用的显式类型
数组,显式地指定相应的 JDBC 类型,通过`registerSqlType`调用
自定义`MapSqlParameterSource`实例,或者通过`List`从 爪哇 声明的属性类型派生出 SQL 类型,即使对于空值也是如此。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.5.3.具有多批处理的批处理操作 前面的批更新示例处理的批是如此之大,以至于你希望将它们分解为几个较小的批。你可以通过多次调用`batchUpdate`方法来使用前面提到的方法来实现这一点,但是现在有了一个更方便的方法。除了 SQL 语句之外,该方法还需要一个包含参数的对象“集合”,每个批处理要进行的更新的数量,以及一个`ParameterizedPreparedStatementSetter`来为准备好的语句的参数值设置值。框架在提供的值上循环,并将更新调用分成指定大小的批。 下面的示例显示了一个使用批处理大小为 100 的批处理更新: 爪哇 ``` public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[][] batchUpdate(final Collection actors) { int[][] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100, (PreparedStatement ps, Actor actor) -> { ps.setString(1, actor.getFirstName()); ps.setString(2, actor.getLastName()); ps.setLong(3, actor.getId().longValue()); }); return updateCounts; } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val jdbcTemplate = JdbcTemplate(dataSource) fun batchUpdate(actors: List): Array { return jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100) { ps, argument -> ps.setString(1, argument.firstName) ps.setString(2, argument.lastName) ps.setLong(3, argument.id) } } // ... additional methods } ``` 此调用的批更新方法返回一个`int`数组的数组,其中包含每个批处理的数组条目,以及每个更新的受影响行数组。顶级数组的长度表示运行的批处理的数量,而二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批处理大小(除了最后一个可能更小),这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个`-2`的值。 ### 3.6.使用`SimpleJdbc`类简化 JDBC 操作 `SimpleJdbcInsert`和`SimpleJdbcCall`类利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了一种简化的配置。这意味着你需要预先配置的内容较少,但是如果你希望在代码中提供所有细节,则可以重写或关闭元数据处理。 #### 3.6.1.使用`SimpleJdbcInsert`插入数据 我们首先查看具有最少配置选项的`isBatchExhausted`类。你应该在数据访问层的初始化方法中实例化`SimpleJdbcInsert`。对于这个示例,初始化方法是“setDataSource”方法。你不需要子类`SimpleJdbcInsert`类。相反,你可以使用`withTableName`方法创建一个新实例并设置表名。该类的配置方法遵循`SimpleJdbcInsert`样式,该样式返回`SimpleJdbcInsert`的实例,它允许你链接所有配置方法。下面的示例只使用一种配置方法(稍后我们将展示多个方法的示例): 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor"); } public void add(Actor actor) { Map parameters = new HashMap(3); parameters.put("id", actor.getId()); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); insertActor.execute(parameters); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor") fun add(actor: Actor) { val parameters = mutableMapOf() parameters["id"] = actor.id parameters["first_name"] = actor.firstName parameters["last_name"] = actor.lastName insertActor.execute(parameters) } // ... additional methods } ``` 这里使用的`execute`方法将一个普通的`java.util.Map`作为其唯一参数。这里需要注意的重要事项是,用于`Map`的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 INSERT 语句。 #### 3.6.2.使用`java.util.Map`检索自动生成的密钥 下一个示例使用与前一个示例相同的 INSERT,但是,它检索自动生成的键并将其设置在新的`Actor`对象上,而不是传入`id`。当它创建`SimpleJdbcInsert`时,除了指定表名外,它还使用`usingGeneratedKeyColumns`方法指定生成的键列的名称。下面的清单展示了它的工作原理: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map parameters = new HashMap(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val insertActor = SimpleJdbcInsert(dataSource) .withTableName("t_actor").usingGeneratedKeyColumns("id") fun add(actor: Actor): Actor { val parameters = mapOf( "first_name" to actor.firstName, "last_name" to actor.lastName) val newId = insertActor.executeAndReturnKey(parameters); return actor.copy(id = newId.toLong()) } // ... additional methods } ``` 在使用第二种方法运行 INSERT 时,主要的区别是你没有将`id`添加到`Map`中,而是调用`executeAndReturnKey`方法。这返回一个’java.lang.number’对象,你可以使用它创建域类中使用的数值类型的实例。在这里,你不能依赖所有的数据库来返回特定的 爪哇 类。`java.lang.Number`是你可以依赖的基类。如果有多个自动生成的列,或者生成的值是非数字的,则可以使用从`executeAndReturnKeyHolder`方法返回的`KeyHolder`。 #### 3.6.3.指定`SimpleJdbcInsert`的列 可以通过使用“usingcolumns”方法指定列名列表来限制插入的列,如下例所示: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingColumns("first_name", "last_name") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map parameters = new HashMap(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val insertActor = SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingColumns("first_name", "last_name") .usingGeneratedKeyColumns("id") fun add(actor: Actor): Actor { val parameters = mapOf( "first_name" to actor.firstName, "last_name" to actor.lastName) val newId = insertActor.executeAndReturnKey(parameters); return actor.copy(id = newId.toLong()) } // ... additional methods } ``` INSERT 的执行与依赖元数据来确定要使用哪些列的情况相同。 #### 3.6.4.使用`SqlParameterSource`提供参数值 使用`Map`提供参数值可以很好地工作,但它不是使用最方便的类。 Spring 提供了[Understanding `SqlQuery`](#jdbc-SqlQuery)接口的两个实现方式,你可以使用这些实现方式来代替。第一个是`BeanPropertySqlParameterSource`,如果你有一个包含你的值的符合 爪哇Bean 的类,那么它是一个非常方便的类。它采用相应的 getter 方法提取参数值。下面的示例展示了如何使用`BeanPropertySqlParameterSource`: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val insertActor = SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id") fun add(actor: Actor): Actor { val parameters = BeanPropertySqlParameterSource(actor) val newId = insertActor.executeAndReturnKey(parameters) return actor.copy(id = newId.toLong()) } // ... additional methods } ``` 另一个选项是`MapSqlParameterSource`,它类似于`Map`,但提供了一个可以链接的更方便的`addValue`方法。下面的示例展示了如何使用它: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new MapSqlParameterSource() .addValue("first_name", actor.getFirstName()) .addValue("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val insertActor = SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id") fun add(actor: Actor): Actor { val parameters = MapSqlParameterSource() .addValue("first_name", actor.firstName) .addValue("last_name", actor.lastName) val newId = insertActor.executeAndReturnKey(parameters) return actor.copy(id = newId.toLong()) } // ... additional methods } ``` 正如你所看到的,配置是相同的。只有执行代码必须更改才能使用这些可选的输入类。 #### 3.6.5.用`SimpleJdbcCall`调用存储过程 `SimpleJdbcCall`类使用数据库中的元数据查找`in`和`in`参数的名称,这样你就不必显式地声明它们。如果你愿意这样做,或者如果你的参数(例如`ARRAY`或`STRUCT`)没有自动映射到 爪哇 类,则可以声明参数。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回`VARCHAR`和`DATE`格式中的标量值。示例过程读取指定的参与者条目,并以`SqlQuery`参数的形式返回 `first_name’、`last_name`和`birth_date`列。下面的清单展示了第一个示例: ``` CREATE PROCEDURE read_actor ( IN in_id INTEGER, OUT out_first_name VARCHAR(100), OUT out_last_name VARCHAR(100), OUT out_birth_date DATE) BEGIN SELECT first_name, last_name, birth_date INTO out_first_name, out_last_name, out_birth_date FROM t_actor where id = in_id; END; ``` 参数`in_id`包含你正在查找的参与者的`id`。`out`参数返回从表中读取的数据。 你可以以类似于声明`SimpleJdbcCall`的方式声明`SimpleJdbcCall`。你应该在数据访问层的初始化方法中实例化和配置类。与`StoredProcedure`类相比,不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。下面的`SimpleJdbcCall`配置示例使用了前面的存储过程(除了`DataSource`之外,唯一的配置选项是存储过程的名称): 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcCall procReadActor; public void setDataSource(DataSource dataSource) { this.procReadActor = new SimpleJdbcCall(dataSource) .withProcedureName("read_actor"); } public Actor readActor(Long id) { SqlParameterSource in = new MapSqlParameterSource() .addValue("in_id", id); Map out = procReadActor.execute(in); Actor actor = new Actor(); actor.setId(id); actor.setFirstName((String) out.get("out_first_name")); actor.setLastName((String) out.get("out_last_name")); actor.setBirthDate((Date) out.get("out_birth_date")); return actor; } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val procReadActor = SimpleJdbcCall(dataSource) .withProcedureName("read_actor") fun readActor(id: Long): Actor { val source = MapSqlParameterSource().addValue("in_id", id) val output = procReadActor.execute(source) return Actor( id, output["out_first_name"] as String, output["out_last_name"] as String, output["out_birth_date"] as Date) } // ... additional methods } ``` 你为执行调用编写的代码涉及创建一个包含 IN 参数的`SqlParameterSource`。你必须将为输入值提供的名称与存储过程中声明的参数名称的名称匹配。这种情况不一定要匹配,因为你使用元数据来确定在存储过程中应该如何引用数据库对象。源文件中为存储过程指定的内容不一定是存储在数据库中的方式。一些数据库将名称转换为所有大写,而另一些数据库则使用小写或使用指定的大小写。 `execute`方法接受 IN 参数并返回一个`Map`,该参数包含由名称键入的任何`out`参数,如在存储过程中指定的那样。在这种情况下,它们是 `out_first_name`,`out_last_name`和`out_birth_date`。 `execute`方法的最后一部分将创建一个`Actor`实例,用于返回检索到的数据。同样,使用在存储过程中声明的`out`参数的名称也很重要。而且,存储在结果映射中的`out`参数的名称与数据库中的`out`参数名称的情况匹配,这在不同的数据库中可能会有所不同。为了使代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用`LinkedCaseInsensitiveMap`。要实现后者,你可以创建自己的`JdbcTemplate`,并将`setResultsMapCaseInsensitive`属性设置为`true`。然后,你可以将这个定制的`JdbcTemplate`实例传递到你的`SimpleJdbcCall`的构造函数中。下面的示例展示了这种配置: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcCall procReadActor; public void setDataSource(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setResultsMapCaseInsensitive(true); this.procReadActor = new SimpleJdbcCall(jdbcTemplate) .withProcedureName("read_actor"); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply { isResultsMapCaseInsensitive = true }).withProcedureName("read_actor") // ... additional methods } ``` 通过执行此操作,可以避免在用于返回的`out`参数的名称的情况下发生冲突。 #### 3.6.6.显式声明用于`SimpleJdbcCall`的参数 在本章的前面,我们描述了参数是如何从元数据推导出来的,但是如果你愿意,你可以显式地声明它们。你可以通过使用`declareParameters`方法创建和配置`SimpleJdbcCall`来实现这一点,该方法接受数量可变的`SqlParameter`对象作为输入。有关如何定义`SqlParameter`的详细信息,请参见[next section](#jdbc-params)。 | |如果你使用的数据库不是 Spring 支持的
数据库,则需要显式声明。目前, Spring 支持
以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。
我们还支持 MySQL、Microsoft SQL Server、
和 Oracle 的存储函数的元数据查找。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以 OPT 显式地声明一个、一些或所有参数。在未显式声明参数的情况下,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理,只使用声明的参数,可以调用方法`withoutProcedureColumnMetaDataAccess`作为声明的一部分。假设你为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,调用`useInParameterNames`来指定给定签名要包含的 IN 参数名列表。 下面的示例展示了一个完全声明的过程调用,并使用了前面示例中的信息: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcCall procReadActor; public void setDataSource(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setResultsMapCaseInsensitive(true); this.procReadActor = new SimpleJdbcCall(jdbcTemplate) .withProcedureName("read_actor") .withoutProcedureColumnMetaDataAccess() .useInParameterNames("in_id") .declareParameters( new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR), new SqlOutParameter("out_last_name", Types.VARCHAR), new SqlOutParameter("out_birth_date", Types.DATE) ); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply { isResultsMapCaseInsensitive = true }).withProcedureName("read_actor") .withoutProcedureColumnMetaDataAccess() .useInParameterNames("in_id") .declareParameters( SqlParameter("in_id", Types.NUMERIC), SqlOutParameter("out_first_name", Types.VARCHAR), SqlOutParameter("out_last_name", Types.VARCHAR), SqlOutParameter("out_birth_date", Types.DATE) ) // ... additional methods } ``` 这两个示例的执行和最终结果是相同的。第二个示例显式地指定了所有细节,而不是依赖于元数据。 #### 3.6.7.如何定义`SqlParameters` 要为`SimpleJdbc`类和 RDBMS 操作类(在[将 JDBC 操作建模为 爪哇 对象](#jdbc-object)中涉及)定义参数,你可以使用`SqlParameter`或它的一个子类。为此,你通常在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用`java.sql.Types`常量指定的。在本章的前面,我们看到了类似的声明: 爪哇 ``` new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR), ``` Kotlin ``` SqlParameter("in_id", Types.NUMERIC), SqlOutParameter("out_first_name", Types.VARCHAR), ``` 带有`SqlParameter`的第一行声明了一个 IN 参数。通过使用`SqlQuery`及其子类(在[Understanding `SqlQuery`](#jdbc-SqlQuery)中涵盖),可以在参数中用于存储过程调用和查询。 第二行(带有`SqlOutParameter`)声明一个`out`参数,该参数将在存储过程调用中使用。对于`InOut`参数也有`SqlInOutParameter`(为过程提供 in 值并返回值的参数)。 | |只有声明为`SqlParameter`和`SqlInOutParameter`的参数用于
提供输入值。这与`StoredProcedure`类不同,后者(出于
向后兼容性的原因)允许为参数
提供输入值,声明为`SqlOutParameter`。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 对于 IN 参数,除了名称和 SQL 类型之外,你还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于`out`参数,可以提供`RowMapper`来处理从`REF`游标返回的行的映射。另一种选择是指定`SqlReturnType`,它提供了定义对返回值的定制处理的机会。 #### 3.6.8.使用`SimpleJdbcCall`调用存储函数 你可以以调用存储过程的几乎相同的方式调用存储函数,只是提供了一个函数名而不是过程名。使用 `WithFunctionName’方法作为配置的一部分,以指示你要对函数进行调用,并生成函数调用的相应字符串。一个专门的调用(“executefunction”)用于运行函数,它将返回函数的返回值作为指定类型的对象,这意味着你不必从结果映射中检索返回值。对于只有一个`out`参数的存储过程,也可以使用类似的方便方法(名为`executeObject`)。下面的示例(对于 MySQL)基于一个名为`get_actor_name`的存储函数,该函数返回参与者的全名: ``` CREATE FUNCTION get_actor_name (in_id INTEGER) RETURNS VARCHAR(200) READS SQL DATA BEGIN DECLARE out_name VARCHAR(200); SELECT concat(first_name, ' ', last_name) INTO out_name FROM t_actor where id = in_id; RETURN out_name; END; ``` 要调用这个函数,我们再次在初始化方法中创建`SimpleJdbcCall`,如下例所示: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcCall funcGetActorName; public void setDataSource(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setResultsMapCaseInsensitive(true); this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate) .withFunctionName("get_actor_name"); } public String getActorName(Long id) { SqlParameterSource in = new MapSqlParameterSource() .addValue("in_id", id); String name = funcGetActorName.executeFunction(String.class, in); return name; } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val jdbcTemplate = JdbcTemplate(dataSource).apply { isResultsMapCaseInsensitive = true } private val funcGetActorName = SimpleJdbcCall(jdbcTemplate) .withFunctionName("get_actor_name") fun getActorName(id: Long): String { val source = MapSqlParameterSource().addValue("in_id", id) return funcGetActorName.executeFunction(String::class.java, source) } // ... additional methods } ``` 所使用的`executeFunction`方法返回一个`SimpleJdbcCall`,它包含函数调用的返回值。 #### 3.6.9.从`SimpleJdbcCall`返回`ResultSet`或 ref 光标 调用返回结果集的存储过程或函数有点困难。一些数据库在 JDBC 结果处理过程中返回结果集,而另一些数据库则需要显式注册特定类型的`out`参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用`SimpleJdbcCall`,你可以使用`returningResultSet`方法并声明用于特定参数的`SimpleJdbcCall`实现。如果在结果处理过程中返回了结果集,则没有定义名称,因此返回的结果必须与声明`RowMapper`实现的顺序匹配。指定的名称仍用于将已处理的结果列表存储在从`execute`语句返回的结果映射中。 下一个示例(对于 MySQL)使用一个存储过程,该过程接受 no in 参数并返回`t_actor`表中的所有行: ``` CREATE PROCEDURE read_all_actors() BEGIN SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a; END; ``` 要调用这个过程,你可以声明`RowMapper`。因为要映射到的类遵循 爪哇Bean 规则,所以可以使用`BeanPropertyRowMapper`,它是通过在`newInstance`方法中传递要映射到的所需类来创建的。下面的示例展示了如何做到这一点: 爪哇 ``` public class JdbcActorDao implements ActorDao { private SimpleJdbcCall procReadAllActors; public void setDataSource(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.setResultsMapCaseInsensitive(true); this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate) .withProcedureName("read_all_actors") .returningResultSet("actors", BeanPropertyRowMapper.newInstance(Actor.class)); } public List getActorsList() { Map m = procReadAllActors.execute(new HashMap(0)); return (List) m.get("actors"); } // ... additional methods } ``` Kotlin ``` class JdbcActorDao(dataSource: DataSource) : ActorDao { private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply { isResultsMapCaseInsensitive = true }).withProcedureName("read_all_actors") .returningResultSet("actors", BeanPropertyRowMapper.newInstance(Actor::class.java)) fun getActorsList(): List { val m = procReadAllActors.execute(mapOf()) return m["actors"] as List } // ... additional methods } ``` `execute`调用传入一个空的`Map`,因为这个调用不接受任何参数。然后从结果映射中检索参与者列表,并将其返回给调用者。 ### 3.7.将 JDBC 操作建模为 爪哇 对象 `org.springframework.jdbc.object`包包含允许你以更面向对象的方式访问数据库的类。例如,你可以运行查询并以列表的形式获得结果,该列表包含业务对象,并将关系列数据映射到业务对象的属性。你还可以运行存储过程和运行更新、删除和插入语句。 | |Spring 许多开发人员认为,下面描述的各种 RDBMS 操作类(类除外)通常可以将替换为直接的调用。通常,编写一个直接在`JdbcTemplate`上调用方法的 DAO
方法更简单(而不是
将查询封装为一个成熟的类)。
但是,如果你要从使用 RDBMS 操作类中获得可测量的值,
你应该继续使用这些类。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.7.1.理解`SqlQuery` `SqlQuery`是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现`newRowMapper(..)`方法,以提供一个
实例,该实例可以通过在执行查询期间创建的`ResultSet`上进行迭代,为每行创建一个对象。很少直接使用`SqlQuery`类,因为`MappingSqlQuery`子类为将行映射到 Java 类提供了一个更方便的实现。扩展`SqlQuery`的其他实现是 `mappingsqlquerywithparameters’和`UpdatableSqlQuery`。 #### 3.7.2.使用`MappingSqlQuery` `MappingSqlQuery`是一个可重用的查询,在该查询中,具体的子类必须实现抽象`mapRow(..)`方法,以将所提供的`ResultSet`中的每一行转换为指定类型的对象。下面的示例显示了一个自定义查询,该查询将来自`t_actor`关系的数据映射到`getClobAsCharacterStream`类的实例: Java ``` public class ActorMappingQuery extends MappingSqlQuery { public ActorMappingQuery(DataSource ds) { super(ds, "select id, first_name, last_name from t_actor where id = ?"); declareParameter(new SqlParameter("id", Types.INTEGER)); compile(); } @Override protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException { Actor actor = new Actor(); actor.setId(rs.getLong("id")); actor.setFirstName(rs.getString("first_name")); actor.setLastName(rs.getString("last_name")); return actor; } } ``` Kotlin ``` class ActorMappingQuery(ds: DataSource) : MappingSqlQuery(ds, "select id, first_name, last_name from t_actor where id = ?") { init { declareParameter(SqlParameter("id", Types.INTEGER)) compile() } override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor( rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name") ) } ``` 该类扩展了`MappingSqlQuery`参数化的`Actor`类型。此客户查询的构造函数将`DataSource`作为唯一的参数。在此构造函数中,你可以使用`DataSource`调用超类上的构造函数,并调用应该运行的 SQL 来检索此查询的行。此 SQL 用于创建`PreparedStatement`,因此它可能包含用于在执行过程中传递的任何参数的占位符。你必须使用`declareParameter`方法声明每个参数,并传入`SqlParameter`。`SqlParameter`接受一个名称和在`java.sql.Types`中定义的 JDBC 类型。在定义了所有参数之后,你可以调用“compile()”方法,这样就可以准备语句并在以后运行它。这个类在编译后是线程安全的,因此,只要在初始化 DAO 时创建了这些实例,它们就可以保留为实例变量并被重用。下面的示例展示了如何定义这样的类: Java ``` private ActorMappingQuery actorMappingQuery; @Autowired public void setDataSource(DataSource dataSource) { this.actorMappingQuery = new ActorMappingQuery(dataSource); } public Customer getCustomer(Long id) { return actorMappingQuery.findObject(id); } ``` Kotlin ``` private val actorMappingQuery = ActorMappingQuery(dataSource) fun getCustomer(id: Long) = actorMappingQuery.findObject(id) ``` 前面示例中的方法使用`id`检索作为唯一参数传入的客户。由于我们只希望返回一个对象,所以我们调用`findObject`便利方法,并以`id`作为参数。如果我们有一个返回对象列表并获取附加参数的查询,那么我们将使用`execute`方法中的一个,该方法接受以 vargs 形式传递的参数值数组。下面的示例展示了这样的方法: Java ``` public List searchForActors(int age, String namePattern) { List actors = actorSearchMappingQuery.execute(age, namePattern); return actors; } ``` Kotlin ``` fun searchForActors(age: Int, namePattern: String) = actorSearchMappingQuery.execute(age, namePattern) ``` #### 3.7.3.使用`SqlUpdate` `SqlUpdate`类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,并且,与所有`RdbmsOperation`类一样,更新可以有参数,并在 SQL 中定义。这个类提供了许多`out`方法,类似于查询对象的 `execute’方法。`SqlUpdate`类是具体的。它可以被子类——例如,添加一个自定义更新方法。但是,你不必对`SqlUpdate`类进行子类,因为可以通过设置 SQL 和声明参数来轻松地对它进行参数化。下面的示例创建了一个名为`execute`的自定义更新方法: Java ``` import java.sql.Types; import javax.sql.DataSource; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.SqlUpdate; public class UpdateCreditRating extends SqlUpdate { public UpdateCreditRating(DataSource ds) { setDataSource(ds); setSql("update customer set credit_rating = ? where id = ?"); declareParameter(new SqlParameter("creditRating", Types.NUMERIC)); declareParameter(new SqlParameter("id", Types.NUMERIC)); compile(); } /** * @param id for the Customer to be updated * @param rating the new value for credit rating * @return number of rows updated */ public int execute(int id, int rating) { return update(rating, id); } } ``` Kotlin ``` import java.sql.Types import javax.sql.DataSource import org.springframework.jdbc.core.SqlParameter import org.springframework.jdbc.object.SqlUpdate class UpdateCreditRating(ds: DataSource) : SqlUpdate() { init { setDataSource(ds) sql = "update customer set credit_rating = ? where id = ?" declareParameter(SqlParameter("creditRating", Types.NUMERIC)) declareParameter(SqlParameter("id", Types.NUMERIC)) compile() } /** * @param id for the Customer to be updated * @param rating the new value for credit rating * @return number of rows updated */ fun execute(id: Int, rating: Int): Int { return update(rating, id) } } ``` #### 3.7.4.使用`StoredProcedure` `StoredProcedure`类是用于 RDBMS 存储过程的对象抽象的`abstract`超类。 继承的`sql`属性是 RDBMS 中存储过程的名称。 要为`StoredProcedure`类定义参数,可以使用`SqlParameter`或它的一个子类。你必须在构造函数中指定参数名和 SQL 类型,如下列代码片段所示: Java ``` new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR), ``` Kotlin ``` SqlParameter("in_id", Types.NUMERIC), SqlOutParameter("out_first_name", Types.VARCHAR), ``` SQL 类型是使用`java.sql.Types`常量指定的。 第一行(带有`SqlParameter`)声明一个 IN 参数。你可以在参数中使用`SqlQuery`及其子类的存储过程调用和查询(在[Understanding `SqlQuery`](#jdbc-SqlQuery)中涵盖)。 第二行(带有`SqlOutParameter`)声明一个`out`参数,该参数将在存储过程调用中使用。对于`InOut`参数也有`SqlInOutParameter`(这些参数为过程提供了`in`值,并且还返回了一个值)。 对于`in`参数,除了名称和 SQL 类型之外,还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于`out`参数,可以提供`RowMapper`来处理从`REF`游标返回的行的映射。另一个选项是指定`SqlReturnType`,它允许你定义对返回值的定制处理。 简单 DAO 的下一个示例使用`StoredProcedure`调用函数 `),该函数随任何 Oracle 数据库一起提供。要使用存储过程功能,你必须创建一个扩展`StoredProcedure`的类。在本例中,`StoredProcedure`类是一个内部类。但是,如果需要重用“storedprocedure”,则可以将其声明为顶级类。这个示例没有输入参数,但是通过使用“sqloutParameter”类将输出参数声明为日期类型。`execute()`方法运行过程,并从结果`Map`中提取返回的日期。通过使用参数名称作为键,结果`Map`为每个声明的输出参数(在本例中只有一个)有一个条目。下面的清单显示了我们的自定义存储过程类: Java ``` import java.sql.Types; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.object.StoredProcedure; public class StoredProcedureDao { private GetSysdateProcedure getSysdate; @Autowired public void init(DataSource dataSource) { this.getSysdate = new GetSysdateProcedure(dataSource); } public Date getSysdate() { return getSysdate.execute(); } private class GetSysdateProcedure extends StoredProcedure { private static final String SQL = "sysdate"; public GetSysdateProcedure(DataSource dataSource) { setDataSource(dataSource); setFunction(true); setSql(SQL); declareParameter(new SqlOutParameter("date", Types.DATE)); compile(); } public Date execute() { // the 'sysdate' sproc has no input parameters, so an empty Map is supplied... Map results = execute(new HashMap()); Date sysdate = (Date) results.get("date"); return sysdate; } } } ``` Kotlin ``` import java.sql.Types import java.util.Date import java.util.Map import javax.sql.DataSource import org.springframework.jdbc.core.SqlOutParameter import org.springframework.jdbc.object.StoredProcedure class StoredProcedureDao(dataSource: DataSource) { private val SQL = "sysdate" private val getSysdate = GetSysdateProcedure(dataSource) val sysdate: Date get() = getSysdate.execute() private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() { init { setDataSource(dataSource) isFunction = true sql = SQL declareParameter(SqlOutParameter("date", Types.DATE)) compile() } fun execute(): Date { // the 'sysdate' sproc has no input parameters, so an empty Map is supplied... val results = execute(mutableMapOf()) return results["date"] as Date } } } ``` 下面的`StoredProcedure`示例有两个输出参数(在本例中是 Oracle Ref 游标): Java ``` import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import oracle.jdbc.OracleTypes; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.object.StoredProcedure; public class TitlesAndGenresStoredProcedure extends StoredProcedure { private static final String SPROC_NAME = "AllTitlesAndGenres"; public TitlesAndGenresStoredProcedure(DataSource dataSource) { super(dataSource, SPROC_NAME); declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper())); declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper())); compile(); } public Map execute() { // again, this sproc has no input parameters, so an empty Map is supplied return super.execute(new HashMap()); } } ``` Kotlin ``` import java.util.HashMap import javax.sql.DataSource import oracle.jdbc.OracleTypes import org.springframework.jdbc.core.SqlOutParameter import org.springframework.jdbc.object.StoredProcedure class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) { companion object { private const val SPROC_NAME = "AllTitlesAndGenres" } init { declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper())) declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper())) compile() } fun execute(): Map { // again, this sproc has no input parameters, so an empty Map is supplied return super.execute(HashMap()) } } ``` 请注意,在`TitlesAndGenresStoredProcedure`构造函数中使用的`declareParameter(..)`方法的重载变量是如何传递`RowMapper`实现实例的。这是一种非常方便且功能强大的重用现有功能的方法。接下来的两个示例提供了两个`RowMapper`实现的代码。 对于所提供的`ResultSet`中的每一行,`TitleMapper`类将`ResultSet`映射到`Title`域对象,如下所示: Java ``` import java.sql.ResultSet; import java.sql.SQLException; import com.foo.domain.Title; import org.springframework.jdbc.core.RowMapper; public final class TitleMapper implements RowMapper { public Title mapRow(ResultSet rs, int rowNum) throws SQLException { Title title = new Title(); title.setId(rs.getLong("id")); title.setName(rs.getString("name")); return title; } } ``` Kotlin ``` import java.sql.ResultSet import com.foo.domain.Title import org.springframework.jdbc.core.RowMapper class TitleMapper : RowMapper<Title> { override fun mapRow(rs: ResultSet, rowNum: Int) = Title(rs.getLong("id"), rs.getString("name")) } ``` 对于所提供的`ResultSet`中的每一行,`SqlParameterValue`类将`ResultSet`映射到`Genre`域对象,如下所示: Java ``` import java.sql.ResultSet; import java.sql.SQLException; import com.foo.domain.Genre; import org.springframework.jdbc.core.RowMapper; public final class GenreMapper implements RowMapper<Genre> { public Genre mapRow(ResultSet rs, int rowNum) throws SQLException { return new Genre(rs.getString("name")); } } ``` Kotlin ``` import java.sql.ResultSet import com.foo.domain.Genre import org.springframework.jdbc.core.RowMapper class GenreMapper : RowMapper<Genre> { override fun mapRow(rs: ResultSet, rowNum: Int): Genre { return Genre(rs.getString("name")) } } ``` 要将参数传递给在 RDBMS 中的定义中具有一个或多个输入参数的存储过程,可以对强类型`execute(..)`方法进行编码,该方法将委托给超类中的非类型`execute(Map)`方法,如下例所示: Java ``` import java.sql.Types; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import oracle.jdbc.OracleTypes; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.StoredProcedure; public class TitlesAfterDateStoredProcedure extends StoredProcedure { private static final String SPROC_NAME = "TitlesAfterDate"; private static final String CUTOFF_DATE_PARAM = "cutoffDate"; public TitlesAfterDateStoredProcedure(DataSource dataSource) { super(dataSource, SPROC_NAME); declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE); declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper())); compile(); } public Map<String, Object> execute(Date cutoffDate) { Map<String, Object> inputs = new HashMap<String, Object>(); inputs.put(CUTOFF_DATE_PARAM, cutoffDate); return super.execute(inputs); } } ``` Kotlin ``` import java.sql.Types import java.util.Date import javax.sql.DataSource import oracle.jdbc.OracleTypes import org.springframework.jdbc.core.SqlOutParameter import org.springframework.jdbc.core.SqlParameter import org.springframework.jdbc.object.StoredProcedure class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) { companion object { private const val SPROC_NAME = "TitlesAfterDate" private const val CUTOFF_DATE_PARAM = "cutoffDate" } init { declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE)) declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper())) compile() } fun execute(cutoffDate: Date) = super.execute( mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate)) } ``` ### 3.8.参数和数据值处理的常见问题 Spring Framework 的 JDBC 支持所提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决这些问题。 #### 3.8.1.为参数提供 SQL 类型信息 通常, Spring 根据传入的参数的类型来确定参数的 SQL 类型。在设置参数值时,可以显式地提供要使用的 SQL 类型。这有时是正确设置`NULL`值所必需的。 你可以通过以下几种方式提供 SQL 类型信息: * `JdbcTemplate`的许多更新和查询方法以`int`数组的形式接受一个额外的参数。此数组用于通过使用来自`java.sql.Types`类的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。 * 你可以使用`SqlParameterValue`类来包装需要此附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。你还可以为数值提供一个可选的缩放参数。 * 对于使用命名参数的方法,可以使用`SqlParameterSource`类,`BeanPropertySQLParameterSource’或`getBlobAsBytes`。它们都有为任何命名参数值注册 SQL 类型的方法。 #### 3.8.2.处理 BLOB 和 CLOB 对象 你可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象被称为 BLOBS(二进制大对象),用于二进制数据,而 CLOBS(字符大对象),用于字符数据。在 Spring 中,可以通过直接使用`JdbcTemplate`处理这些大型对象,也可以在使用 RDBMS 对象和`SimpleJdbc`类提供的更高抽象时处理这些对象。所有这些方法都使用`LobHandler`接口的实现,用于实际管理 LOB(大对象)数据。`LOB 处理程序 ` 通过`LobCreator`方法提供对`LobCreator`类的访问,即用于创建要插入的新的 LOB 对象。 `LobCreator`和`LobHandler`为 LOB 输入和输出提供以下支持: * BLOB * `byte[]`:`getBlobAsBytes`和`setBlobAsBytes` * `InputStream`:`getBlobAsBinaryStream`和`setBlobAsBinaryStream` * CLOB * `String`:`getClobAsString`和`setClobAsString` * `InputStream`:`getClobAsAsciiStream`和`setClobAsAsciiStream` * `Reader`:`getClobAsCharacterStream`和`setClobAsCharacterStream` 下一个示例展示了如何创建和插入 BLOB。稍后,我们将展示如何从数据库中读回它。 这个示例使用`JdbcTemplate`和 `AbstractLobcreatingPreparedStatementCallback’的实现。它实现了一个方法“setvalues”。这个方法提供了`LobCreator`,我们使用它来设置 SQL INSERT 语句中 LOB 列的值。 对于这个示例,我们假设有一个变量`lobHandler`,它已经被设置为`DefaultLobHandler`的实例。你通常通过依赖项注入来设置该值。 下面的示例展示了如何创建和插入 BLOB: Java ``` final File blobIn = new File("spring2004.jpg"); final InputStream blobIs = new FileInputStream(blobIn); final File clobIn = new File("large.txt"); final InputStream clobIs = new FileInputStream(clobIn); final InputStreamReader clobReader = new InputStreamReader(clobIs); jdbcTemplate.execute( "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)", new AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1) protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { ps.setLong(1, 1L); lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); (2) lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); (3) } } ); blobIs.close(); clobReader.close(); ``` |**1**|在`declareParameter`中传递(在本例中)是一个普通的`DefaultLobHandler`。| |-----|--------------------------------------------------------------------------------| |**2**|使用`setClobAsCharacterStream`的方法来传入 CLOB 的内容。| |**3**|使用`setBlobAsBinaryStream`方法传入 BLOB 的内容。| Kotlin ``` val blobIn = File("spring2004.jpg") val blobIs = FileInputStream(blobIn) val clobIn = File("large.txt") val clobIs = FileInputStream(clobIn) val clobReader = InputStreamReader(clobIs) jdbcTemplate.execute( "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)", object: AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1) override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) { ps.setLong(1, 1L) lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt()) (2) lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt()) (3) } } ) blobIs.close() clobReader.close() ``` |**1**|在`lobHandler`中传递(在本例中)是一个普通的`DefaultLobHandler`。| |-----|--------------------------------------------------------------------------------| |**2**|使用`setClobAsCharacterStream`方法传入 CLOB 的内容。| |**3**|使用`setBlobAsBinaryStream`方法传入 BLOB 的内容。| | |如果在从 `defaultLobhandler.getlobCreator()’返回的<gtr="1338"/>上调用<gtr="1336"/>、<gtr="1337"/>或 `setClobasCharacterStream’方法,则可以为 `ContentLength’参数指定一个负值。如果指定的内容长度为负,“DefaultLobHandler”将使用 set-stream 方法的 JDBC4.0 变体,而不使用<br/>长度参数。否则,它会将指定的长度传递给驱动程序。<br/><br/>查看用于验证它是否支持流<br/>LOB 而不提供内容长度的 JDBC 驱动程序的文档。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 现在是从数据库中读取 LOB 数据的时候了。同样,使用带有相同实例变量`JdbcTemplate`的`lobHandler`和对`DefaultLobHandler`的引用。下面的示例展示了如何做到这一点: Java ``` List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table", new RowMapper<Map<String, Object>>() { public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException { Map<String, Object> results = new HashMap<String, Object>(); String clobText = lobHandler.getClobAsString(rs, "a_clob"); (1) results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); (2) results.put("BLOB", blobBytes); return results; } }); ``` |**1**|使用`getClobAsString`方法检索 CLOB 的内容。| |-----|------------------------------------------------------------------------| |**2**|使用方法`getBlobAsBytes`检索 blob 的内容。| Kotlin ``` val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ -> val clobText = lobHandler.getClobAsString(rs, "a_clob") (1) val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob") (2) mapOf("CLOB" to clobText, "BLOB" to blobBytes) } ``` |**1**|使用方法`getClobAsString`检索 CLOB 的内容。| |-----|------------------------------------------------------------------------| |**2**|使用方法`getBlobAsBytes`检索 blob 的内容。| #### 3.8.3.传入 IN 子句的值列表 SQL 标准允许基于包含变量值列表的表达式选择行。一个典型的例子是`select * from T_ACTOR where id in (1, 2, 3)`。JDBC 标准对准备好的语句不直接支持此变量列表。不能声明数量可变的占位符。你需要使用所需数量的占位符来进行一些更改,或者,一旦知道需要多少个占位符,就需要动态地生成 SQL 字符串。在`NamedParameterJdbcTemplate`和`JdbcTemplate`中提供的命名参数支持采用后一种方法。可以将这些值作为基元对象的`java.util.List`传入。这个列表用于插入所需的占位符,并在语句执行期间传入这些值。 | |在传递许多值时要小心。JDBC 标准并不保证<br/>表达式列表可以使用超过 100 个值。各种数据库都超过了<br/>的值,但是它们通常对允许的值有一个严格的限制。例如,甲骨文的<br/>极限是 1000。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 除了 value 列表中的基本值之外,你还可以创建对象数组的`java.util.List`。这个列表可以支持为`in`子句定义的多个表达式,例如`select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))`。当然,这需要你的数据库支持这种语法。 #### 3.8.4.处理存储过程调用的复杂类型 当调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型, Spring 提供了一个`SqlReturnType`,用于在从存储过程调用返回它们时处理它们,以及在将它们作为参数传入存储过程时处理`SqlTypeValue`。 `SqlReturnType`接口有一个必须实现的方法(名为`getTypeValue`)。该接口用作`SqlOutParameter`声明的一部分。下面的示例显示返回用户声明类型`ITEM_TYPE`的 Oracle`STRUCT`对象的值: Java ``` public class TestItemStoredProcedure extends StoredProcedure { public TestItemStoredProcedure(DataSource dataSource) { // ... declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE", (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { STRUCT struct = (STRUCT) cs.getObject(colIndx); Object[] attr = struct.getAttributes(); TestItem item = new TestItem(); item.setId(((Number) attr[0]).longValue()); item.setDescription((String) attr[1]); item.setExpirationDate((java.util.Date) attr[2]); return item; })); // ... } ``` Kotlin ``` class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { init { // ... declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName -> val struct = cs.getObject(colIndx) as STRUCT val attr = struct.getAttributes() TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date) }) // ... } } ``` 可以使用`SqlTypeValue`将 Java 对象的值(例如`TestItem`)传递给存储过程。`SqlTypeValue`接口有一个必须实现的方法(名为 `createtypeValue’)。活动连接被传入,你可以使用它创建特定于数据库的对象,例如`StructDescriptor`实例或`ArrayDescriptor`实例。下面的示例创建了`StructDescriptor`实例: Java ``` final TestItem testItem = new TestItem(123L, "A test item", new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); SqlTypeValue value = new AbstractSqlTypeValue() { protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn); Struct item = new STRUCT(itemDescriptor, conn, new Object[] { testItem.getId(), testItem.getDescription(), new java.sql.Date(testItem.getExpirationDate().getTime()) }); return item; } }; ``` Kotlin ``` val (id, description, expirationDate) = TestItem(123L, "A test item", SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) val value = object : AbstractSqlTypeValue() { override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { val itemDescriptor = StructDescriptor(typeName, conn) return STRUCT(itemDescriptor, conn, arrayOf(id, description, java.sql.Date(expirationDate.time))) } } ``` 现在可以将这个`SqlTypeValue`添加到`Map`中,该参数包含存储过程的 `execute’调用的输入参数。 `SqlTypeValue`的另一种用法是将一组值传递给 Oracle 存储过程。Oracle 有自己的内部`ARRAY`类,在这种情况下必须使用它,你可以使用`SqlTypeValue`来创建 Oracle`ARRAY`的实例,并用来自 Java`ARRAY`的值填充它,如下例所示: Java ``` final Long[] ids = new Long[] {1L, 2L}; SqlTypeValue value = new AbstractSqlTypeValue() { protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn); ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids); return idArray; } }; ``` Kotlin ``` class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { init { val ids = arrayOf(1L, 2L) val value = object : AbstractSqlTypeValue() { override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { val arrayDescriptor = ArrayDescriptor(typeName, conn) return ARRAY(arrayDescriptor, conn, ids) } } } } ``` ### 3.9.嵌入式数据库支持 `org.springframework.jdbc.datasource.embedded`包提供了对嵌入式 Java 数据库引擎的支持。本地提供了对[HSQL](http://www.hsqldb.org)、[H2](https://www.h2database.com)和[Derby](https://db.apache.org/derby)的支持。你还可以使用可扩展的 API 来插入新的嵌入式数据库类型和“数据源”实现。 #### 3.9.1.为什么要使用嵌入式数据库? 由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。好处包括易于配置、快速启动时间、可测试性以及在开发过程中快速改进 SQL 的能力。 #### 3.9.2.使用 Spring XML 创建嵌入式数据库 如果希望在 Spring `ApplicationContext’中以 Bean 的形式公开嵌入式数据库实例,则可以在`embedded-database`名称空间中使用`spring-jdbc`标记: ``` <jdbc:embedded-database id="dataSource" generate-name="true"> <jdbc:script location="classpath:schema.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database> ``` 前面的配置创建了一个嵌入式 HSQL 数据库,该数据库由来自 Classpath 根中`schema.sql`和`test-data.sql`资源的 SQL 填充。此外,作为一种最佳实践,嵌入式数据库被分配一个唯一生成的名称。将嵌入式数据库作为类型为“javax.sql.datasource”的 Bean 容器提供给 Spring 容器,然后可以根据需要将其注入到数据访问对象中。 #### 3.9.3.以编程方式创建嵌入式数据库 `EmbeddedDatabaseBuilder`类为以编程方式构建嵌入式数据库提供了一个 Fluent API。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此方法,如下例所示: Java ``` EmbeddedDatabase db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build(); // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) db.shutdown() ``` Kotlin ``` val db = EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build() // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) db.shutdown() ``` 有关所有支持的选项的更多详细信息,请参见[javadoc for `EmbeddedDatabaseBuilder`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html)。 你还可以使用`EmbeddedDatabaseBuilder`通过使用 Java 配置来创建嵌入式数据库,如下例所示: Java ``` @Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build(); } } ``` Kotlin ``` @Configuration class DataSourceConfig { @Bean fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build() } } ``` #### 3.9.4.选择嵌入式数据库类型 本节介绍如何在 Spring 支持的三个嵌入式数据库中选择一个。它包括以下主题: * [使用 HSQL](#jdbc-embedded-database-using-HSQL) * [使用 H2](#jdbc-embedded-database-using-H2) * [使用 Derby](#jdbc-embedded-database-using-Derby) ##### Using HSQL Spring 支持 HSQL1.8.0 及以上版本。如果没有显式指定类型,则 HSQL 是默认的嵌入式数据库。要显式地指定 HSQL,请将 `embedded-database’标记的`type`属性设置为`HSQL`。如果使用 Builder API,则使用`EmbeddedDatabaseType.HSQL`调用“settype”方法。 ##### Using H2 Spring 支持 H2 数据库。要启用 H2,请将 `embedded-database’标记的`type`属性设置为`H2`。如果使用 Builder API,则使用`EmbeddedDatabaseType.H2`调用“settype”方法。 ##### Using Derby Spring 支持 Apache Derby10.5 及以上版本。要启用 Derby,请将`embedded-database`标记的`type`属性设置为`DERBY`。如果使用 Builder API,则使用`EmbeddedDatabaseType.DERBY`调用`setType(EmbeddedDatabaseType)`方法。 #### 3.9.5.用嵌入式数据库测试数据访问逻辑 嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性操作很有用。但是,如果你希望创建在测试套件中共享的嵌入式数据库,可以考虑使用[Spring TestContext Framework](testing.html#testcontext-framework),并将嵌入式数据库配置为 Bean 中的 Spring `ApplicationContext`中的 Bean,如[Creating an Embedded Database by Using Spring XML](#jdbc-embedded-database-xml)和[以编程方式创建嵌入式数据库](#jdbc-embedded-database-java)中所述。下面的清单显示了测试模板: Java ``` public class DataAccessIntegrationTestTemplate { private EmbeddedDatabase db; @BeforeEach public void setUp() { // creates an HSQL in-memory database populated from default scripts // classpath:schema.sql and classpath:data.sql db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .addDefaultScripts() .build(); } @Test public void testDataAccess() { JdbcTemplate template = new JdbcTemplate(db); template.query( /* ... */ ); } @AfterEach public void tearDown() { db.shutdown(); } } ``` Kotlin ``` class DataAccessIntegrationTestTemplate { private lateinit var db: EmbeddedDatabase @BeforeEach fun setUp() { // creates an HSQL in-memory database populated from default scripts // classpath:schema.sql and classpath:data.sql db = EmbeddedDatabaseBuilder() .generateUniqueName(true) .addDefaultScripts() .build() } @Test fun testDataAccess() { val template = JdbcTemplate(db) template.query( /* ... */) } @AfterEach fun tearDown() { db.shutdown() } } ``` #### 3.9.6.为嵌入式数据库生成唯一的名称 如果开发团队的测试套件无意中试图重新创建同一数据库的其他实例,那么在使用嵌入式数据库时,开发团队经常会遇到错误。如果一个 XML 配置文件或`@Configuration`类负责创建嵌入式数据库,并且相应的配置随后在相同的测试套件(即在相同的 JVM 流程中)的多个测试场景中被重用,那么这种情况就很容易发生,例如,针对嵌入式数据库的集成测试,其“ApplicationContext”配置仅在 Bean 定义配置文件处于活动状态时有所不同。 此类错误的根本原因是 Spring 的`EmbeddedDatabaseFactory`(`<jdbc:embedded-database>`XML 名称空间元素和用于 Java 配置的 `EmbedDedatabaseBuilder’内部都使用)将嵌入式数据库的名称设置为 `TestDB’,如果没有另外指定的话。对于`<jdbc:embedded-database>`的情况,嵌入式数据库通常被分配一个等于 Bean 的`id`的名称(通常,类似于`dataSource`)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且试图创建新的嵌入式数据库实际上指向从相同配置创建的现有嵌入式数据库。 为了解决这个常见的问题, Spring Framework4.2 提供了为嵌入式数据库生成唯一名称的支持。要启用生成的名称,请使用以下选项之一。 * `EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()` * `EmbeddedDatabaseBuilder.generateUniqueName()` * `<jdbc:embedded-database generate-name="true" …​ >` #### 3.9.7.扩展嵌入式数据库支持 你可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持: * 实现`EmbeddedDatabaseConfigurer`以支持新的嵌入式数据库类型。 * 实现`DataSourceFactory`以支持新的`DataSource`实现,例如连接池来管理嵌入式数据库连接。 我们鼓励你在[GitHub Issues](https://github.com/spring-projects/spring-framework/issues)上为 Spring 社区提供扩展。 ### 3.10.初始化`DataSource` `org.springframework.jdbc.datasource.init`包支持初始化现有的`DataSource`。嵌入式数据库支持提供了一个选项,用于为应用程序创建和初始化`DataSource`。然而,有时你可能需要初始化在某个服务器上运行的实例。 #### 3.10.1.使用 Spring XML 初始化数据库 如果要初始化数据库,并且可以提供对`DataSource` Bean 的引用,则可以在`initialize-database`名称空间中使用`initialize-database`标记: ``` <jdbc:initialize-database data-source="dataSource"> <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/> <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/> </jdbc:initialize-database> ``` 前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符的模式,通配符是用于 Spring 中的资源的常见 Ant 样式(例如,` Classpath *:/com/foo/**/SQL/*-data.sql`)。如果使用模式,那么脚本将按照其 URL 或文件名的词法顺序运行。 数据库初始化器的默认行为是无条件地运行所提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循先创建表,然后插入数据的常见模式(如前面所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步失败。 然而,为了获得对现有数据的创建和删除的更多控制,XML 名称空间提供了一些附加选项。第一个是用来打开和关闭初始化的标志。你可以根据环境来设置这个值(例如,从系统属性或环境中提取布尔值 Bean)。下面的示例从系统属性获得一个值: ``` <jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1) <jdbc:script location="..."/> </jdbc:initialize-database> ``` |**1**|从一个名为`INITIALIZE_DATABASE`的系统属性获取`enabled`的值。| |-----|--------------------------------------------------------------------------------| 控制现有数据的第二个选择是更宽容地对待失败。为此,你可以控制初始化器的能力,以忽略它从脚本运行的 SQL 中的某些错误,如下例所示: ``` <jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS"> <jdbc:script location="..."/> </jdbc:initialize-database> ``` 在前面的示例中,我们认为,有时脚本是针对一个空数据库运行的,因此脚本中的一些`DROP`语句将会失败。因此失败的 SQL`DROP`语句将被忽略,但其他失败将导致异常。如果你的 SQL 方言不支持`DROP …​ IF EXISTS`(或类似),但你希望在重新创建测试数据之前无条件地删除所有测试数据,那么这是非常有用的。在这种情况下,第一个脚本通常是一组`DROP`语句,然后是一组`CREATE`语句。 `ignore-failures`选项可以设置为`NONE`(默认值),`DROPS`(忽略失败的下降),或`ALL`(忽略所有失败)。 如果脚本中根本不存在`;`字符,则每个语句都应该用`;`分隔,或者用新的一行分隔。你可以全局控制它,也可以通过脚本控制它,如下例所示: ``` <jdbc:initialize-database data-source="dataSource" separator="@@"> (1) <jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2) <jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/> <jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/> </jdbc:initialize-database> ``` |**1**|将分隔符脚本设置为`@@`。| |-----|---------------------------------------------| |**2**|将`db-schema.sql`的分隔符设置为`;`。| 在这个示例中,两个`test-data`脚本使用`@@`作为语句分隔符,只有`db-schema.sql`使用`;`。此配置指定了`@@`的默认分隔符,并覆盖了`db-schema`脚本的默认分隔符。 如果你需要的控制比从 XML 命名空间获得的更多,那么你可以直接使用“DataSourceInitializer”并将其定义为应用程序中的组件。 ##### 依赖于数据库的其他组件的初始化 一大类应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以使用数据库初始化器,而不会有更多的麻烦。如果你的应用程序不是其中之一,那么你可能需要阅读本节的其余部分。 数据库初始化器依赖于`DataSource`实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的`init-method`,组件中的`@PostConstruct`方法,或实现`InitializingBean`的组件中的`afterPropertiesSet()`方法)。如果其他 bean 依赖于相同的数据源,并在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化。这方面的一个常见示例是一个缓存,它在应用程序启动时急切地初始化并从数据库加载数据。 要解决这个问题,你有两种选择:将缓存初始化策略更改到稍后的阶段,或者确保首先初始化数据库初始化器。 如果应用程序在你的控制范围内,而不是在其他情况下,更改缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括: * 使缓存在第一次使用时就进行惰性初始化,从而提高了应用程序的启动时间。 * 拥有你的缓存或初始化缓存实现“生命周期”或`SmartLifecycle`的独立组件。当应用程序上下文启动时,可以通过设置其`autoStartup`标志自动启动`SmartLifecycle`,也可以通过在封闭的上下文上调用`ConfigurableApplicationContext.start()`手动启动`Lifecycle`。 * 使用 Spring `ApplicationEvent`或类似的自定义观察者机制来触发缓存初始化。`ContextRefreshedEvent`总是在上下文准备好使用时发布(在所有 bean 都已初始化之后),因此这通常是一个有用的钩子(这是`SmartLifecycle`默认情况下的工作方式)。 确保首先初始化数据库初始化器也很容易。关于如何实现这一点的一些建议包括: * 依赖于 Spring `BeanFactory`的默认行为,即以注册顺序初始化 bean。通过在 XML 配置中采用一组`<import/>`元素的常见做法来安排这一点,这些元素对应用程序模块进行了排序,并确保首先列出了数据库和数据库初始化。 * 分离`DataSource`和使用它的业务组件,并通过将它们放在单独的`ApplicationContext`实例中来控制它们的启动顺序(例如,父上下文包含`DataSource`,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用。 ## 4. 使用 R2DBC 进行数据访问 [R2DBC](https://r2dbc.io)(“反应式关系数据库连接”)是一种社区驱动的规范工作,用于使用反应式模式标准化对 SQL 数据库的访问。 ### 4.1.包层次结构 Spring 框架的 R2DBC 抽象框架由两个不同的包组成: * `core`:`org.springframework.r2dbc.core`包包含`DatabaseClient`类和各种相关的类。见[使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理](#r2dbc-core)。 * `connection`:`org.springframework.r2dbc.connection`包包含一个用于 easy`ConnectionFactory`访问的实用程序类和各种简单的`ConnectionFactory`实现,你可以使用这些实现来测试和运行未修改的 R2DBC。见[控制数据库连接](#r2dbc-connections)。 ### 4.2.使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理 本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。它包括以下主题: * [Using `DatabaseClient`](#r2dbc-DatabaseClient) * [执行语句](#r2dbc-DatabaseClient-examples-statement) * [查询(“选择”)](#r2dbc-DatabaseClient-examples-query) * [Updating (`INSERT`, `UPDATE`, and `DELETE`) with `DatabaseClient`](#r2dbc-DatabaseClient-examples-update) * [语句过滤器](#r2dbc-DatabaseClient-filter) * [检索自动生成的密钥](#r2dbc-auto-generated-keys) #### 4.2.1.使用`DatabaseClient` `DatabaseClient`是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见的错误,例如忘记关闭连接。它执行核心 R2DBC 工作流的基本任务(例如语句创建和执行),将应用程序代码留给提供 SQL 和提取结果。`DatabaseClient`类: * 运行 SQL 查询 * 更新语句和存储过程调用 * 在`Result`实例上执行迭代 * 捕获 R2DBC 异常,并将其转换为在`org.springframework.dao`包中定义的通用的、信息量更大的异常层次结构。(见[一致的异常层次结构](#dao-exceptions)。 客户机有一个功能强大、流畅的 API,它使用了用于声明性组合的反应性类型。 当你使用`DatabaseClient`作为你的代码时,你只需要实现 `java.util.function’接口,并为它们提供一个明确定义的契约。给定一个由`DatabaseClient`类提供的`Connection`,一个`Function`回调将创建一个`Publisher`。对于提取`Row`结果的映射函数也是如此。 你可以通过使用`ConnectionFactory`引用的直接实例化在 DAO 实现中使用`DatabaseClient`,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。 创建`DatabaseClient`对象的最简单方法是通过静态工厂方法,如下所示: Java ``` DatabaseClient client = DatabaseClient.create(connectionFactory); ``` Kotlin ``` val client = DatabaseClient.create(connectionFactory) ``` | |在 Spring IOC<br/>容器中,应该始终将`ConnectionFactory`配置为 Bean。| |---|----------------------------------------------------------------------------------------------| 前面的方法使用默认设置创建`DatabaseClient`。 你还可以从`DatabaseClient.builder()`获得`Builder`实例。你可以通过调用以下方法来定制客户机: * `….bindMarkers(…)`:提供一个特定的`BindMarkersFactory`以将命名参数配置为数据库绑定标记转换。 * `….executeFunction(…)`:设置`ExecuteFunction`如何运行`Statement`对象。 * `….namedParameters(false)`:禁用命名参数展开。默认情况下启用。 | |方言是由[BindMarkersFactoryResolver](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.html)从`ConnectionFactory`解析的,通常是通过检查`ConnectionFactoryMetadata`。<br/>你可以通过注册一个<br/>实现`org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider`的类,让 Spring 自动发现你的`BindMarkersFactory`。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 目前支持的数据库有: * H2 * 马里亚布 * Microsoft SQL Server * MySQL * Postgres 这个类发布的所有 SQL 都记录在`DEBUG`级别下,该类别对应于客户端实例的完全限定类名称(通常为 `DefaultDatabaseClient’)。此外,每个执行都在响应序列中注册一个检查点,以帮助调试。 下面的部分提供了`DatabaseClient`用法的一些示例。这些示例并不是`DatabaseClient`公开的所有功能的详尽列表。参见乘务员[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/core/DatabaseClient.html)。 ##### 执行语句 `DatabaseClient`提供了运行语句的基本功能。下面的示例展示了创建新表的最小但功能齐全的代码需要包括哪些内容: Java ``` Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") .then(); ``` Kotlin ``` client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") .await() ``` `DatabaseClient`是为方便、流畅的使用而设计的。它在执行规范的每个阶段公开中间方法、延续方法和终端方法。上面的示例使用`then()`返回一个完成的 `publisher’,该’publisher’在查询(或查询,如果 SQL 查询包含多个语句)完成后立即完成。 | |`execute(…)`接受 SQL 查询字符串或查询`Supplier<String>`,以将实际的查询创建推迟到执行时。| |---|---------------------------------------------------------------------------------------------------------------------------------| ##### 查询(“选择”) SQL 查询可以通过`Row`对象或受影响的行数返回值。`DatabaseClient’可以返回更新的行数或行本身,具体取决于发出的查询。 下面的查询从表中获取`id`和`name`列: Java ``` Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person") .fetch().first(); ``` Kotlin ``` val first = client.sql("SELECT id, name FROM person") .fetch().awaitSingle() ``` 以下查询使用了一个绑定变量: 爪哇 ``` Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") .bind("fn", "Joe") .fetch().first(); ``` Kotlin ``` val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn") .bind("fn", "Joe") .fetch().awaitSingle() ``` 你可能已经注意到在上面的示例中使用了`fetch()`。`fetch()`是一个延续运算符,它允许你指定要消耗多少数据。 调用`first()`返回结果中的第一行,并丢弃剩余的行。你可以使用以下操作符来使用数据: * `first()`返回整个结果的第一行。它的 Kotlin 协程变量名为`awaitSingle()`,用于表示不可空的返回值,如果该值是可选的,则名为`awaitSingleOrNull()`。 * `one()`只返回一个结果,如果结果包含更多行,则该结果将失败。使用 Kotlin 协程,`awaitOne()`正好代表一个值,或者`awaitOneOrNull()`如果该值可能是`null`。 * `all()`返回结果的所有行。当使用 Kotlin 协程时,使用`flow()`。 * `rowsUpdated()`返回受影响的行数。其 Kotlin 协程变体被命名为`awaitRowsUpdated()`。 在不指定更多映射细节的情况下,查询以`Map`的形式返回表式结果,其键是不区分大小写的列名,这些列名映射到它们的列值。 你可以通过提供一个`Function<Row, T>`来控制结果映射,每个`Row`都会被调用,这样它就可以返回任意值(奇异值、集合和映射以及对象)。 下面的示例提取`name`列并发出其值: 爪哇 ``` Flux<String> names = client.sql("SELECT name FROM person") .map(row -> row.get("name", String.class)) .all(); ``` Kotlin ``` val names = client.sql("SELECT name FROM person") .map{ row: Row -> row.get("name", String.class) } .flow() ``` 那么`null`呢? 关系数据库结果可以包含`null`值。反应流规范禁止`null`值的发射。该需求要求在提取器函数中进行适当的`null`处理。虽然可以从`Row`中获得`null`值,但不能发出`null`值。你必须包装对象中的任何`null`值(例如,对于奇异值,`Optional`),以确保提取函数永远不会直接返回`null`值。 ##### 用`DatabaseClient`更新(` 插入 `,`UPDATE`,和`DELETE`)##### 修改语句的唯一区别是,这些语句通常不返回表格数据,因此你可以使用`rowsUpdated()`来使用结果。 下面的示例显示了返回更新行数的`UPDATE`语句: 爪哇 ``` Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn") .bind("fn", "Joe") .fetch().rowsUpdated(); ``` Kotlin ``` val affectedRows = client.sql("UPDATE person SET first_name = :fn") .bind("fn", "Joe") .fetch().awaitRowsUpdated() ``` ##### 将值绑定到查询 典型的应用程序需要参数化的 SQL 语句来根据某些输入选择或更新行。这些语句通常是`SELECT`语句,受`WHERE`子句或`INSERT`和`UPDATE`语句的约束,这些语句接受输入参数。如果参数没有正确转义,参数化语句将承担 SQL 注入的风险。`DatabaseClient`利用 R2DBC 的 `bind’API 来消除查询参数的 SQL 注入风险。你可以使用`execute(…)`操作符提供参数化 SQL 语句,并将参数绑定到实际的`Statement`。然后,你的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行该语句。 参数绑定支持两种绑定策略: * 通过索引,使用零基参数索引。 * 按名称,使用占位符名称。 下面的示例展示了查询的参数绑定: ``` db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34); ``` R2DBC 原生结合标记 R2DBC 使用依赖于实际数据库供应商的数据库本地绑定标记。例如,Postgres 使用索引标记,例如`$1`,`$2`,`$n`。另一个例子是 SQL Server,它使用带有`@`前缀的命名绑定标记。 这与 JDBC 不同,后者需要`?`作为绑定标记。在 JDBC 中,实际的驱动程序将`?`绑定标记转换为数据库本地标记,作为其语句执行的一部分。 Spring Framework 的 R2DBC 支持允许你使用本机绑定标记或带有`:name`语法的命名绑定标记。 命名参数支持利用`BindMarkersFactory`实例在执行查询时将命名参数扩展为本机绑定标记,这使你在不同的数据库供应商之间具有一定程度的查询可移植性。 查询预处理程序将名为`Collection`的参数展开到一系列绑定标记中,以消除基于参数数量的动态查询创建的需要。嵌套对象数组被扩展以允许使用(例如)选择列表。 考虑以下查询: ``` SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) ``` 前面的查询可以参数化并按以下方式运行: 爪哇 ``` List<Object[]> tuples = new ArrayList<>(); tuples.add(new Object[] {"John", 35}); tuples.add(new Object[] {"Ann", 50}); client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") .bind("tuples", tuples); ``` Kotlin ``` val tuples: MutableList<Array<Any>> = ArrayList() tuples.add(arrayOf("John", 35)) tuples.add(arrayOf("Ann", 50)) client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") .bind("tuples", tuples) ``` | |选择列表的使用依赖于供应商。| |---|------------------------------------------| 下面的示例显示了一个使用`IN`谓词的更简单的变体: 爪哇 ``` client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") .bind("ages", Arrays.asList(35, 50)); ``` Kotlin ``` val tuples: MutableList<Array<Any>> = ArrayList() tuples.add(arrayOf("John", 35)) tuples.add(arrayOf("Ann", 50)) client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") .bind("tuples", arrayOf(35, 50)) ``` | |R2DBC 本身不支持类集合的值。尽管如此,<br/>在上面的示例中扩展给定的`List`对于 Spring 的 R2DBC 支持中的命名参数<br/>有效,例如,如上面所示,用于`IN`子句。<br/>但是,插入或更新数组类型的列(例如在 postgres 中)<br/>需要底层 R2DBC 驱动程序支持的数组类型:<br/>通常是一个 爪哇 数组,例如。`String[]`以更新`text[]`列。<br/>不要将`Collection<String>`等作为数组参数传递。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 语句过滤器 有时,你需要在运行前对实际`Statement`上的选项进行微调。通过`DatabaseClient`注册一个`Statement`过滤器(`statementfilterfunction`),以便在语句的执行中拦截和修改语句,如下例所示: 爪哇 ``` client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) .bind("name", …) .bind("state", …); ``` Kotlin ``` client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) } .bind("name", …) .bind("state", …) ``` `DatabaseClient`公开还简化了`filter(…)`接受`Function<Statement, Statement>`的过载: 爪哇 ``` client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter(statement -> s.returnGeneratedValues("id")); client.sql("SELECT id, name, state FROM table") .filter(statement -> s.fetchSize(25)); ``` Kotlin ``` client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { statement -> s.returnGeneratedValues("id") } client.sql("SELECT id, name, state FROM table") .filter { statement -> s.fetchSize(25) } ``` `StatementFilterFunction`实现允许过滤 ` 语句’和过滤`Result`对象。 ##### `DatabaseClient`最佳实践 一旦配置好,`DatabaseClient`类的实例是线程安全的。这很重要,因为这意味着你可以配置`DatabaseClient`的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。`DatabaseClient`是有状态的,因为它保持了对`ConnectionFactory`的引用,但是这个状态不是会话状态。 在使用`DatabaseClient`类时,一种常见的做法是在 Spring 配置文件中配置`ConnectionFactory`,然后在依赖项中注入共享的`ConnectionFactory` Bean 到 DAO 类中。在 setter 中为`ConnectionFactory`创建了`DatabaseClient`。这导致 DAO 类似于以下内容: 爪哇 ``` public class R2dbcCorporateEventDao implements CorporateEventDao { private DatabaseClient databaseClient; public void setConnectionFactory(ConnectionFactory connectionFactory) { this.databaseClient = DatabaseClient.create(connectionFactory); } // R2DBC-backed implementations of the methods on the CorporateEventDao follow... } ``` Kotlin ``` class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { private val databaseClient = DatabaseClient.create(connectionFactory) // R2DBC-backed implementations of the methods on the CorporateEventDao follow... } ``` 显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以使用`@Component`对类进行注释(这使它成为组件扫描的候选),并使用`ConnectionFactory`setter 方法对`@Autowired`进行注释。下面的示例展示了如何做到这一点: 爪哇 ``` @Component (1) public class R2dbcCorporateEventDao implements CorporateEventDao { private DatabaseClient databaseClient; @Autowired (2) public void setConnectionFactory(ConnectionFactory connectionFactory) { this.databaseClient = DatabaseClient.create(connectionFactory); (3) } // R2DBC-backed implementations of the methods on the CorporateEventDao follow... } ``` |**1**|用`@Component`注释类。| |-----|-----------------------------------------------------------------| |**2**|用`@Autowired`注释`ConnectionFactory`setter 方法。| |**3**|用`ConnectionFactory`创建一个新的`DatabaseClient`。| Kotlin ``` @Component (1) class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2) private val databaseClient = DatabaseClient(connectionFactory) (3) // R2DBC-backed implementations of the methods on the CorporateEventDao follow... } ``` |**1**|用`@Component`注释类。| |-----|-----------------------------------------------------------| |**2**|`ConnectionFactory`的构造函数注入。| |**3**|用`ConnectionFactory`创建一个新的`DatabaseClient`。| 无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建`DatabaseClient`类的新实例。一旦配置好,`DatabaseClient`实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个`DatabaseClient`实例,这需要多个 `ConnectionFactory’实例,然后需要多个配置不同的`DatabaseClient`实例。 ### 4.3.检索自动生成的密钥 `INSERT`语句在向定义自动增量或标识列的表中插入行时可能会生成键。要获得对要生成的列名的完全控制,只需注册一个`StatementFilterFunction`,它为所需的列请求生成的键。 爪哇 ``` Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter(statement -> s.returnGeneratedValues("id")) .map(row -> row.get("id", Integer.class)) .first(); // generatedId emits the generated key once the INSERT statement has finished ``` Kotlin ``` val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") .filter { statement -> s.returnGeneratedValues("id") } .map { row -> row.get("id", Integer.class) } .awaitOne() // generatedId emits the generated key once the INSERT statement has finished ``` ### 4.4.控制数据库连接 本节内容包括: * [Using `ConnectionFactory`](#r2dbc-ConnectionFactory) * [Using `ConnectionFactoryUtils`](#r2dbc-ConnectionFactoryUtils) * [Using `SingleConnectionFactory`](#r2dbc-SingleConnectionFactory) * [Using `TransactionAwareConnectionFactoryProxy`](#r2dbc-TransactionAwareConnectionFactoryProxy) * [Using `R2dbcTransactionManager`](#r2dbc-R2dbcTransactionManager) #### 4.4.1.使用`ConnectionFactory` Spring 通过`ConnectionFactory`获得到数据库的 R2DBC 连接。a`ConnectionFactory`是 R2DBC 规范的一部分,是驱动程序的一个常见入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置`ConnectionFactory`的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。 当你使用 Spring 的 R2DBC 层时,你可以使用第三方提供的连接池实现来配置你自己的连接池。一个流行的实现是 R2DBC 池(“R2DBC-Pool”)。 Spring 发行版中的实现仅用于测试目的,并不提供池。 要配置`ConnectionFactory`: 1. 获得与`ConnectionFactory`的连接,就像你通常获得 R2dbc`ConnectionFactory`一样。 2. 提供一个 R2DBC URL(请参阅驱动程序的文档以获得正确的值)。 下面的示例展示了如何配置`ConnectionFactory`: 爪哇 ``` ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); ``` Kotlin ``` val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); ``` #### 4.4.2.使用`ConnectionFactoryUtils` `ConnectionFactoryUtils`类是一个方便且功能强大的助手类,它提供`static`方法来从`ConnectionFactory`获得连接和紧密连接(如果需要)。 它支持订阅者`Context`绑定的连接,例如“R2DBcTransactionManager”。 #### 4.4.3.使用`SingleConnectionFactory` `SingleConnectionFactory`类是`DelegatingConnectionFactory`接口的一种实现,该接口封装单个`Connection`,该接口在每次使用后都不会关闭。 如果任何客户机代码在假定池连接的情况下调用`close`(如使用持久性工具时),则应将`suppressClose`属性设置为`true`。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为本机`Connection`或类似的对象。 `SingleConnectionFactory`主要是一个测试类,如果你的 R2DBC 驱动程序允许这样的使用,它可以用于特定的需求,例如流水线。与池`ConnectionFactory`相反,它始终重用相同的连接,避免了过多地创建物理连接。 #### 4.4.4.使用`TransactionAwareConnectionFactoryProxy` `TransactionAwareConnectionFactoryProxy`是目标`ConnectionFactory`的代理。代理封装了目标`ConnectionFactory`,以添加对 Spring 管理的事务的感知。 | |如果你使用的 R2DBC 客户机不是集成的,则需要使用这个类,否则将支持 Spring 的 R2DBC。在这种情况下,你仍然可以使用这个客户机,并且在<br/>的同时,让这个客户机参与 Spring 托管事务。通常情况下,<br/>最好是将具有对`ConnectionFactoryUtils`的正确访问的 R2DBC 客户机集成在一起,以进行资源管理。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 有关更多详细信息,请参见[“TransactionAwareConnectionFactoryProxy”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html)爪哇doc。 #### 4.4.5.使用`R2dbcTransactionManager` `R2dbcTransactionManager`类是单个 R2DBC 数据源的`ReactiveTransactionManager`实现。它将从指定的连接工厂到订阅服务器`Context`的 R2DBC 连接绑定,可能允许每个连接工厂使用一个订阅服务器连接。 应用程序代码需要通过“ConnectionFactoryUtils.getConnection”来检索 R2DBC 连接,而不是 R2DBC 的标准“ConnectionFactory.Create()”。 所有框架类(如`DatabaseClient`)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。 `R2dbcTransactionManager`类支持应用于连接的自定义隔离级别。 ## 5. 对象关系映射数据访问 本节介绍使用对象关系映射时的数据访问。 ### 5.1.ORM 简介 Spring Spring 框架支持与 爪哇 持久性 API( JPA)的集成,并支持用于资源管理、数据访问对象实现和事务策略的本机 Hibernate。例如,对于 Hibernate,具有几个方便的 IOC 特性的一流支持,这些特性解决了许多典型的 Hibernate 集成问题。你可以通过依赖项注入来配置 OR(对象关系)映射工具的所有支持的特性。它们可以参与 Spring 的资源和事务管理,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成风格是针对普通的 Hibernate 或 JPA API 对 DAO 进行编码。 Spring 在创建数据访问应用程序时,为你选择的 ORM 层增加了显著的增强。你可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与内部构建类似基础设施的成本和风险进行比较。无论采用何种技术,你都可以像使用库一样使用大量的 ORM 支持,因为所有的东西都被设计为一组可重用的 爪哇Bean。 Spring IOC 容器中的 ORM 促进了配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内的配置。 使用 Spring 框架创建 ORM DAO 的好处包括: * **更容易的测试。** Spring 的 IOC 方法使得很容易交换 Hibernate `SessionFactory`实例、JDBC`DataSource`实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。这进而使得隔离地测试与持久性相关的每一段代码变得容易得多。 * **常见的数据访问异常。** Spring 可以从你的 ORM 工具中包装异常,将它们从专有的(可能经过检查的)异常转换为通用的运行时“DataAccessException”层次结构。这个特性允许你仅在适当的层中处理大多数持久性异常(这些异常是不可恢复的),而不需要烦人的样板文件捕获、抛出和异常声明。你仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于 DB 的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。 * **一般资源管理。** Spring 应用程序上下文可以处理 Hibernate `SessionFactory`实例、 JPA `EntityManagerFactory`实例、JDBC`DataSource`实例的位置和配置,以及其他相关资源。这使得这些价值观易于管理和改变。 Spring 提供了对持久性资源的高效、简单和安全的处理。例如,使用 Hibernate 的相关代码通常需要使用相同的 Hibernate `Session`,以确保效率和适当的事务处理。 Spring 通过将当前的`Session`通过 Hibernate `SessionFactory`公开,使得创建`Session`并将其透明地绑定到当前线程变得容易。因此, Spring 解决了 Hibernate 典型使用的许多慢性问题,适用于任何本地或 JTA 事务环境。 * **综合事务管理。**你可以使用声明性的、面向方面的编程( AOP)风格的方法拦截器包装你的 ORM 代码,或者通过 `@Transactional’注释,或者通过在 XML 配置文件中显式地配置事务 AOP 通知。在这两种情况下,事务语义和异常处理(回滚等)都是为你处理的。正如[资源和事务管理](#orm-resource-mngmnt)中所讨论的,你还可以交换各种事务管理器,而不会影响与 ORM 相关的代码。例如,你可以在本地事务和 JTA 之间进行交换,在这两个场景中都可以使用相同的完整服务(例如声明式事务)。此外,与 JDBC 相关的代码可以完全集成到用于执行 ORM 的代码中。这对于不适合 ORM 的数据访问(例如批处理和 BLOB 流)很有用,但仍然需要与 ORM 操作共享公共事务。 | |有关更全面的 ORM 支持,包括对替代数据库<br/>技术(如 MongoDB)的支持,你可能想查看[Spring Data](https://projects.spring.io/spring-data/)项目套件。如果你是<br/> JPA 用户,[Getting Started Accessing<br/>Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)中的[https://spring.io](https://spring.io)指南提供了一个很好的介绍。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 5.2.ORM 集成的一般考虑因素 本节重点介绍了适用于所有 ORM 技术的注意事项。[Hibernate](#orm-hibernate)部分提供了更多细节,并在一个具体的上下文中显示了这些特性和配置。 Spring 的 ORM 集成的主要目标是清晰的应用程序分层(使用任何数据访问和事务技术),并实现应用程序对象的松耦合——不再需要业务服务依赖于数据访问或事务策略,不再需要硬编码的资源查找,不再需要难以替换的单件件,没有更多的自定义服务注册中心。我们的目标是使用一种简单且一致的方法来连接应用程序对象,使它们尽可能地可重用且不依赖于容器。所有单独的数据访问功能都可以单独使用,但与 Spring 的应用程序上下文概念很好地集成在一起,提供了基于 XML 的配置和对普通 爪哇Bean 实例的交叉引用,而这些实例不需要 Spring 知道。 Spring 在典型的应用程序中,许多重要的对象是 爪哇Bean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象的业务服务和事务管理器、Web 视图解析器、使用业务服务的 Web 控制器,等等。 #### 5.2.1.资源和事务管理 典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。 Spring 提倡用于适当的资源处理的简单解决方案,即在 JDBC 的情况下通过模板化来实现 IOC,并为 ORM 技术应用 AOP 拦截器。 基础架构提供了正确的资源处理,并将特定的 API 异常适当地转换为未经检查的基础架构异常层次结构。 Spring 引入了一种 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,`JdbcTemplate`中提到的`JdbcTemplate`类提供连接处理和`SQLException`到 `DataAccessException’层次结构的正确转换,包括将数据库特定的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参见[next section](#orm-exception-translation),了解如何获得相同的异常转换好处。 当涉及到事务管理时,`JdbcTemplate`类连接到 Spring 事务支持,并通过相应的 Spring 事务管理器支持 JTA 和 JDBC 事务。对于所支持的 ORM 技术, Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。有关事务支持的详细信息,请参见[事务管理](#transaction)章节。 #### 5.2.2.异常转换 在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。根据技术的不同,DAO 抛出了`HibernateException`或`PersistenceException`的子类。这些异常都是运行时异常,不需要声明或捕获。你可能还需要处理“非法论题”和`IllegalStateException`。这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久性技术自身的异常结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 ORM 或不需要任何特殊异常处理(或两者兼而有之)的应用程序来说可能是可以接受的。然而, Spring 允许通过`@Repository`注释透明地应用异常翻译。下面的示例(一个用于 爪哇 配置,一个用于 XML 配置)展示了如何这样做: 爪哇 ``` @Repository public class ProductDaoImpl implements ProductDao { // class body here... } ``` Kotlin ``` @Repository class ProductDaoImpl : ProductDao { // class body here... } ``` ``` <beans> <!-- Exception translation bean post processor --> <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/> <bean id="myProductDao" class="product.ProductDaoImpl"/> </beans> ``` 后处理器会自动查找所有异常翻译器(`PersistenceExceptionTranslator`接口的实现),并建议所有标有 `@Repository’注释的 bean,以便发现的翻译器可以拦截并在抛出的异常上应用适当的翻译。 总之,你可以基于纯持久性技术的 API 和注释实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要的话)到 Spring 的自定义异常层次结构。 ### 5.3. Hibernate 我们从在 Spring 环境中覆盖[Hibernate 5](https://hibernate.org/)开始,使用它来演示 Spring 对集成或映射程序所采用的方法。本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体。这些模式中的大多数可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术,并展示简要的示例。 | |在 Spring Framework5.3 中, Spring 对于 Spring 的“HibernateJPavendorAdapter”以及对于原生的 Hibernate `SessionFactory`setup.<br/>对于新启动的应用程序,强烈建议使用 Hibernate orm5.4。,<br/>用于`HibernateJpaVendorAdapter`, Hibernate 搜索需要升级到 5.1 1.6。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.3.1.`SessionFactory` Spring 容器中的设置 为了避免将应用程序对象绑定到硬编码资源查找,可以将资源(例如 JDBC或 Hibernate )定义为 Spring 容器中的 bean。需要访问资源的应用程序对象通过 Bean 引用接收对此类预定义实例的引用,如[next section](#orm-hibernate-straight)中的 DAO 定义所示。 以下摘录自 XML 应用程序上下文定义,展示了如何在它上面设置一个 JDBC`DataSource`和一个 Hibernate `SessionFactory`: ``` <beans> <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="myDataSource"/> <property name="mappingResources"> <list> <value>product.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <value> hibernate.dialect=org.hibernate.dialect.HSQLDialect </value> </property> </bean> </beans> ``` 从本地的 Jakarta Commons DBCP`BasicDataSource`切换到位于 JNDI 的 `DataSource’(通常由应用程序服务器管理)只是一个配置问题,如下例所示: ``` <beans> <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/> </beans> ``` 你还可以访问位于`SessionFactory`的 JNDI,使用 Spring 的 `jndiObjectFactoryBean`/`<jee:jndi-lookup>`来检索和公开它。然而,在 EJB 上下文之外,这种情况通常并不常见。 | |Spring 还提供了一个`LocalSessionFactoryBuilder`变体,将<br/>与`@Bean`样式配置和编程设置无缝集成在一起(不涉及`FactoryBean`)。<br/><br/>既`LocalSessionFactoryBean`又`LocalSessionFactoryBuilder`支持后台<br/>引导, Hibernate 初始化与给定的引导程序执行器上的应用程序<br/>引导程序线程并行运行(例如`SimpleAsyncTaskExecutor`)。<br/>在`LocalSessionFactoryBean`上,这可以通过`bootstrapExecutor`属性获得。在程序化的`LocalSessionFactoryBuilder`上,有一个重载的 `buildsessionFactory’方法,它接受一个 Bootstrap 执行器参数。<br/><br/>从 Spring Framework5.1 开始,这样的本机 Hibernate 设置还可以将用于标准 JPA 交互的[EntityManagerFactory]公开到本机 Hibernate 访问。<br/>详见[Native Hibernate Setup for JPA](#orm-jpa-hibernate)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.3.2.基于普通 Hibernate API 实现 DAOS Hibernate 具有称为上下文会话的功能,其中 Hibernate 本身管理每个事务的当前`Session`。这大致相当于 Spring 的每笔交易同步一次 Hibernate `Session`。基于普通的 Hibernate API,相应的 DAO 实现类似于以下示例: Java ``` public class ProductDaoImpl implements ProductDao { private SessionFactory sessionFactory; public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public Collection loadProductsByCategory(String category) { return this.sessionFactory.getCurrentSession() .createQuery("from test.Product product where product.category=?") .setParameter(0, category) .list(); } } ``` Kotlin ``` class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao { fun loadProductsByCategory(category: String): Collection<*> { return sessionFactory.currentSession .createQuery("from test.Product product where product.category=?") .setParameter(0, category) .list() } } ``` 这种样式类似于 Hibernate 引用文档和示例的样式,只是在实例变量中持有`SessionFactory`。在 Hibernate 的 CaveaTemptor 示例应用程序中的老式`static``HibernateUtil`类之上,我们强烈推荐这样一种基于实例的设置。(一般来说,除非绝对必要,否则不要将任何资源保留在“静态”变量中。) 前面的 DAO 示例遵循依赖注入模式。它非常适合 Spring IOC 容器,就像它与 Spring 的`HibernateTemplate`编码一样。你也可以在普通 Java 中设置这样的 DAO(例如,在单元测试中)。要这样做,实例化它,并使用所需的工厂引用调用`setSessionFactory(..)`。作为 Spring Bean 的定义,DAO 类似于以下内容: ``` <beans> <bean id="myProductDao" class="product.ProductDaoImpl"> <property name="sessionFactory" ref="mySessionFactory"/> </bean> </beans> ``` 这种 DAO 样式的主要优点是它仅依赖于 Hibernate API。不需要任何 Spring 类的进口。从非侵犯性的角度来看,这是有吸引力的,并且对于开发人员来说可能感觉更自然。 然而,DAO 抛出了普通的`HibernateException`(未选中,因此不必声明或捕获),这意味着调用者只能将异常视为通常是致命的——除非他们希望依赖 Hibernate 自己的异常层次结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 Hibernate 的应用程序、不需要任何特殊的异常处理,或者两者兼而有之的应用程序来说都是可以接受的。 幸运的是, Spring 的`LocalSessionFactoryBean`支持 Hibernate 的 `SessionFactory.getCurrentSession()’方法用于任何 Spring 事务策略,返回当前 Spring-管理的事务`Session`,甚至使用 `HibernateTransactionManager’。该方法的标准行为仍然是返回当前与正在进行的 JTA 事务关联的`Session`(如果有的话)。无论你是使用 Spring 的“JTATRansactionManager”、EJB 容器管理事务,还是使用 JTA,这种行为都适用。 总之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。 #### 5.3.3.声明式事务划分 我们建议你使用 Spring 的声明式事务支持,它允许你用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。你可以使用 Java 注释或 XML 在 Spring 容器中配置此事务拦截器。这种声明性事务功能使你能够使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的真正价值所在。 | |在你继续之前,我们强烈建议你阅读[声明式事务管理](#transaction-declarative),如果你还没有这样做。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以使用`@Transactional`注释对服务层进行注释,并指示 Spring 容器查找这些注释,并为这些注释的方法提供事务语义。下面的示例展示了如何做到这一点: Java ``` public class ProductServiceImpl implements ProductService { private ProductDao productDao; public void setProductDao(ProductDao productDao) { this.productDao = productDao; } @Transactional public void increasePriceOfAllProductsInCategory(final String category) { List productsToChange = this.productDao.loadProductsByCategory(category); // ... } @Transactional(readOnly = true) public List<Product> findAllProducts() { return this.productDao.findAllProducts(); } } ``` Kotlin ``` class ProductServiceImpl(private val productDao: ProductDao) : ProductService { @Transactional fun increasePriceOfAllProductsInCategory(category: String) { val productsToChange = productDao.loadProductsByCategory(category) // ... } @Transactional(readOnly = true) fun findAllProducts() = productDao.findAllProducts() } ``` 在容器中,你需要设置`PlatformTransactionManager`实现(作为 Bean)和`<tx:annotation-driven/>`条目,在运行时选择进入`@Transactional`处理。下面的示例展示了如何做到这一点: ``` <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- SessionFactory, DataSource, etc. omitted --> <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven/> <bean id="myProductService" class="product.SimpleProductService"> <property name="productDao" ref="myProductDao"/> </bean> </beans> ``` #### 5.3.4.程序化事务划分 你可以在应用程序的较高级别上,在跨越任意数量操作的较低级别的数据访问服务之上,对事务进行划分。对周围业务服务的实现也不存在限制。它只需要一个 Spring“平台交易管理器”。同样,后者可以来自任何地方,但优选地通过`setTransactionManager(..)`方法作为 Bean 参考。此外,“productDAO”应该由`setProductDao(..)`方法设置。以下一对片段显示了 Spring 应用程序上下文中的事务管理器和业务服务定义,以及业务方法实现的示例: ``` <beans> <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="mySessionFactory"/> </bean> <bean id="myProductService" class="product.ProductServiceImpl"> <property name="transactionManager" ref="myTxManager"/> <property name="productDao" ref="myProductDao"/> </bean> </beans> ``` Java ``` public class ProductServiceImpl implements ProductService { private TransactionTemplate transactionTemplate; private ProductDao productDao; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); } public void setProductDao(ProductDao productDao) { this.productDao = productDao; } public void increasePriceOfAllProductsInCategory(final String category) { this.transactionTemplate.execute(new TransactionCallbackWithoutResult() { public void doInTransactionWithoutResult(TransactionStatus status) { List productsToChange = this.productDao.loadProductsByCategory(category); // do the price increase... } }); } } ``` Kotlin ``` class ProductServiceImpl(transactionManager: PlatformTransactionManager, private val productDao: ProductDao) : ProductService { private val transactionTemplate = TransactionTemplate(transactionManager) fun increasePriceOfAllProductsInCategory(category: String) { transactionTemplate.execute { val productsToChange = productDao.loadProductsByCategory(category) // do the price increase... } } } ``` Spring 的`TransactionInterceptor`允许使用回调码抛出任何选中的应用程序异常,而`TransactionTemplate`仅限于回调中未选中的异常。`TransactionTemplate`在未检查应用程序异常的情况下,或者如果应用程序将事务标记为只回滚(通过设置`TransactionStatus`),则触发回滚。默认情况下,`TransactionInterceptor`的行为与此相同,但每个方法都允许可配置的回滚策略。 #### 5.3.5.事务管理策略 `TransactionTemplate`和`TransactionInterceptor`都将实际的事务处理委托给一个`PlatformTransactionManager`实例(它可以是一个 `HibernateTransactionManager’(对于单个 Hibernate `SessionFactory`),通过使用一个 `threadlocal’`Session`在引擎盖下)或一个`JtaTransactionManager`(委托给容器的 JTA 子系统)用于 Hibernate 应用程序。你甚至可以使用自定义的“Platform TransactionManager”实现。 Hibernate 从本机事务管理切换到 JTA(例如,当你的应用程序的某些部署面临分布式事务需求时)只是一个配置问题。你可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都可以在不做任何更改的情况下工作,因为它们使用了通用事务管理 API。 对于跨多个 Hibernate 会话工厂的分布式事务,你可以将“JTATRansactionManager”作为事务策略与多个“localSessionFactoryBean”定义结合起来。然后,每个 DAO 获得一个特定的`SessionFactory`引用,该引用被传递到其相应的 Bean 属性中。如果所有底层 JDBC 数据源都是事务容器数据源,那么业务服务可以在不需要特别注意的情况下在任意数量的 DAO 和任意数量的会话工厂之间划分事务,只要它使用`JtaTransactionManager`作为策略。 `HibernateTransactionManager`和`JtaTransactionManager`都允许使用 Hibernate 进行适当的 JVM 级缓存处理,而不需要容器特定的事务管理器查找或 JCA 连接器(如果不使用 EJB 发起事务)。 `HibernateTransactionManager`可以将 Hibernate JDBC`Connection`导出为特定`DataSource`的普通 JDBC 访问代码。如果只访问一个数据库,这种能力允许在完全不使用 JTA 的情况下使用混合 Hibernate 和 JDBC 数据访问的高级事务划分。`HibernateTransactionManager`如果你通过 `localSessionFactoryBean’类的`dataSource`属性设置了带有`DataSource`的传入的 `SessionFactory’事务,则自动将 Hibernate 事务公开为 JDBC 事务。或者,你可以通过`HibernateTransactionManager`类的 `DataSource’属性显式地指定事务应该公开的 `DataSource’。 #### 5.3.6.比较容器管理的资源和本地定义的资源 你可以在容器管理的 JNDI`SessionFactory`和本地定义的 JNDI 之间进行切换,而无需更改一行应用程序代码。是将资源定义保留在容器中,还是保留在应用程序的本地中,主要取决于你使用的事务策略。与 Spring 定义的本地“SessionFactory”相比,手动注册的 JNDI不提供任何好处。通过 Hibernate 的 JCA 连接器部署`SessionFactory`提供了参与 Java EE 服务器的管理基础架构的附加价值,但不会在此之外增加实际价值。 Spring 的事务支持不绑定到容器。当使用 JTA 以外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下, Spring 的单资源本地事务支持是 JTA 的一种轻量级和强大的替代方案。当使用本地 EJB 无状态会话 bean 来驱动事务时,你既依赖于 EJB 容器,也依赖于 JTA,即使你只访问一个数据库,并且仅使用无状态会话 bean 通过容器管理的事务来提供声明性事务。以编程方式直接使用 JTA 还需要一个 Java EE 环境。JTA 并不只涉及 JTA 本身和 JNDI`DataSource`实例方面的容器依赖关系。对于非 Spring、JTA 驱动的 Hibernate 事务,你必须使用 Hibernate JCA 连接器或额外的 Hibernate 事务代码,并配置用于适当的 JVM 级缓存的 `TransactionManagerLookup’。 Spring-驱动的事务可以与本地定义的 Hibernate `SessionFactory’一起工作,就像它们与本地 JDBC一起工作一样,只要它们访问单个数据库。因此,当你有分布式事务需求时,你只需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。与部署具有本地资源定义和 Spring 驱动事务的简单 Web 应用程序相比,这种配置需要更多的工作。此外,如果你使用 WebLogic Express(它不提供 JCA),则通常需要容器的 Enterprise 版本。 Spring 具有跨越单个数据库的本地资源和事务的应用程序在任何 Java EE Web 容器(不包括 JTA、JCA 或 EJB)中工作,例如 Tomcat、Resin,甚至是普通的 Jetty。此外,你可以轻松地在桌面应用程序或测试套件中重用这样的中间层。 考虑到所有因素,如果不使用 EJB,请坚持使用本地`SessionFactory`设置和 Spring 的`HibernateTransactionManager`或`JtaTransactionManager`。你获得了所有的好处,包括适当的事务 JVM 级缓存和分布式事务,而不会带来容器部署的不便。 Hibernate `SessionFactory`通过 JCA 连接器进行的 JNDI 注册仅在与 EJB 结合使用时才会增加价值。 #### 5.3.7.带有 Hibernate 的虚假应用程序服务器警告 在一些具有非常严格的`XADataSource`实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当 Hibernate 在不考虑该环境的 JTA 事务管理器的情况下进行配置时,可能会在应用程序服务器日志中显示虚假的警告或异常。这些警告或异常表示正在访问的连接不再有效或 JDBC 访问不再有效,这可能是因为事务不再活动。举个例子,这里有一个来自 WebLogic 的实际例外: ``` java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No further JDBC access is allowed within this transaction. ``` 另一个常见的问题是 JTA 事务之后的连接泄漏, Hibernate 会话(以及潜在的底层 JDBC 连接)无法正确关闭。 你可以通过使 Hibernate 了解 JTA 事务管理器来解决此类问题,JTA 事务管理器与 Spring 同步到该事务管理器。你有两个选择: * 将你的 Spring `JtaTransactionManager` Bean 传递到你的 Hibernate 设置中。最简单的方法是 Bean 引用`jtaTransactionManager`属性中的“localSessionFactoryBean” Bean(参见[Hibernate Transaction Setup](#transaction-strategies-hibernate))。 Spring 然后使相应的 JTA 策略可用于 Hibernate。 * 你还可以在`LocalSessionFactoryBean`上的“HibernateProperties”中显式地配置 Hibernate 的 JTA 相关属性,特别是“ Hibernate.transaction.coordinator\_class”、“ Hibernate.connection.handling\_mode”和潜在的“ Hibernate.transaction.jta.platform”(有关这些属性的详细信息,请参见 Hibernate 的手册)。 本节的其余部分描述了在 Hibernate 意识到 JTA的情况下发生的事件的顺序。 Hibernate 未配置任何对 JTA 事务管理器的了解时,当 JTA 事务提交时发生以下事件: * 提交 JTA 事务。 * Spring 的`JtaTransactionManager`与 JTA 事务同步,因此 JTA 事务管理器通过`afterCompletion`回调回调它。 * 在其他活动中,这种同步可以通过 Hibernate 的`afterTransactionCompletion`回调(用于清除 Hibernate 缓存)触发 Spring 到 Hibernate 的回调,然后在 Hibernate 会话上执行显式的`close()`调用,这会导致 Hibernate 尝试`close()`JDBC 连接。 * 在某些环境中,这个`Connection.close()`调用会触发警告或错误,因为应用程序服务器不再认为`Connection`是可用的,因为事务已经提交。 Hibernate 配置了对 JTA 事务管理器的感知时,当 JTA 事务提交时发生以下事件: * JTA 事务已准备好提交。 * Spring 的`JtaTransactionManager`与 JTA 事务同步,因此 JTA 事务管理器通过`beforeCompletion`回调回调该事务。 * Spring 意识到 Hibernate 本身与 JTA 事务同步,并且其行为与在前面的场景中不同。特别是,它与 Hibernate 的事务性资源管理保持一致。 * 提交 JTA 事务。 * Hibernate 是同步到 JTA 事务的,因此该事务通过 JTA 事务管理器的`afterCompletion`回调被调回,并且可以正确地清除其缓存。 ### 5.4. JPA Spring JPA,在包下可用,以类似于与 Hibernate 集成的方式提供对的全面支持,同时意识到底层实现,以便提供额外的特征。 #### 5.4.1.在 Spring 环境中用于 JPA 设置的三个选项 Spring JPA 支持提供了设置 JPA 的三种方式,其被应用程序用于获得实体管理器。 * [使用`LocalEntityManagerFactoryBean`](#orm-jpa-setup-lemfb) * [从 JNDI 获得 EntityManagerFactory](#orm-jpa-setup-jndi) * [使用`LocalContainerEntityManagerFactoryBean`](#orm-jpa-setup-lcemfb) ##### Using `LocalEntityManagerFactoryBean` 你只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。 `LocalEntityManagerFactoryBean`创建了一个`EntityManagerFactory`,适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 Bean 使用 JPA `PersistenceProvider`自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要指定持久性单元名称。下面的 XML 示例配置了这样的 Bean: ``` <beans> <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"> <property name="persistenceUnitName" value="myPersistenceUnit"/> </bean> </beans> ``` JPA 部署的这种形式是最简单和最有限的。你无法引用现有的 JDBC`DataSource` Bean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节代码转换)是特定于提供者的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于 JPA 规范为其设计的独立应用程序和测试环境。 ##### 从 JNDI 获得 EntityManagerFactory 在部署到 Java EE 服务器时可以使用此选项。检查你的服务器的文档,了解如何将自定义 JPA 提供程序部署到你的服务器中,从而允许使用不同于服务器默认值的提供程序。 从 JNDI(例如在 Java EE 环境中)获得`EntityManagerFactory`是更改 XML 配置的问题,如下例所示: ``` <beans> <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/> </beans> ``` 此操作假定了标准的 Java EE 引导。Java EE 服务器自动检测持久化单元(实际上是应用程序 JAR 中的`META-INF/persistence.xml`文件)和 Java EE 部署描述符中的“持久化-单元-ref”条目(例如,“web.xml”),并为这些持久化单元定义环境命名上下文位置。 在这样的场景中,整个持久性单元部署,包括持久性类的编织(字节代码转换),由 Java EE 服务器决定。JDBC“数据源”是通过`META-INF/persistence.xml`文件中的一个 JNDI 位置定义的。“EntityManager”事务与服务器的 JTA 子系统集成在一起。 Spring 仅使用获得的`EntityManagerFactory`,通过依赖注入将其传递给应用程序对象,并为持久性单元管理事务(通常通过`JtaTransactionManager`)。 如果在同一个应用程序中使用多个持久化单元,则此类 JNDI 检索的持久化单元的 Bean 名称应与应用程序用于引用它们的持久化单元名称相匹配(例如,在`@PersistenceUnit`和 `@persistentencecontext’注释中)。 ##### Using `LocalContainerEntityManagerFactoryBean` 你可以在基于 Spring 的应用程序环境中使用此选项来获得完整的 JPA 功能。这包括诸如 Tomcat 之类的 Web 容器、独立应用程序以及具有复杂持久性需求的集成测试。 | |如果你想要具体地配置一个 Hibernate 设置,那么一个直接的替代方案<br/>是设置一个本机 Hibernate `LocalSessionFactoryBean`,而不是一个普通的 JPA `localContaineRentityManageryBean’,让它与 JPA 访问代码<br/>以及本机 Hibernate 访问代码进行交互。<br/>有关详细信息,请参见[Native Hibernate setup for JPA interaction](#orm-jpa-hibernate)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| `LocalContainerEntityManagerFactoryBean`提供了对“EntityManagerFactory”配置的完全控制,适用于需要细粒度定制的环境。`LocalContainerEntityManagerFactoryBean`基于`persistence.xml`文件、提供的`dataSourceLookup`策略和指定的`loadTimeWeaver`创建一个`PersistenceUnitInfo`实例。因此,可以使用 JNDI 之外的自定义数据源并控制编织过程。下面的示例显示了“localContainerentyManagerFactoryBean”的典型定义 Bean: ``` <beans> <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="someDataSource"/> <property name="loadTimeWeaver"> <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/> </property> </bean> </beans> ``` 下面的示例显示了一个典型的`persistence.xml`文件: ``` <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL"> <mapping-file>META-INF/orm.xml</mapping-file> <exclude-unlisted-classes/> </persistence-unit> </persistence> ``` | |`<exclude-unlisted-classes/>`快捷方式表示不应该对<br/>带注释的实体类进行扫描。显式的’true’值<br/>(`<exclude-unlisted-classes>true</exclude-unlisted-classes/>`)也表示没有扫描。`<exclude-unlisted-classes>false</exclude-unlisted-classes/>` 确实会触发扫描。<br/>但是,如果你希望发生实体类扫描,我们建议省略`exclude-unlisted-classes`元素<br/>。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| JPA 使用`LocalContainerEntityManagerFactoryBean`是最强大的设置选项,允许在应用程序中进行灵活的本地配置。它支持到现有 JDBC`DataSource`的链接,支持本地和全局事务,以此类推。然而,它也对运行时环境提出了要求,例如,如果持久性提供程序要求进行字节码转换,则需要一个具有编织能力的类装入器。 此选项可能与 Java EE 服务器的内置 JPA 功能相冲突。在完整的 Java EE 环境中,考虑从 JNDI 获得`EntityManagerFactory`。或者,在你的“localContaineRentyManagerFactoryBean”定义(例如,meta-inf/my-Persistence.xml)上指定一个自定义<gtr="1857"/>,并在应用程序 jar 文件中仅包含带有该名称的描述符。因为 Java EE 服务器只查找默认的 `meta-inf/persistence.xml’文件,所以它忽略了这些自定义的持久性单元,因此避免了与 Spring 驱动的 JPA 预先设置的冲突。(例如,这适用于树脂 3.1) 什么时候需要进行负载-时间编织? 并不是所有 JPA 提供者都需要 JVM 代理。 Hibernate 是一个不这样做的例子。如果你的提供者不需要代理,或者你有其他的选择,例如在构建时通过自定义编译器或 Ant 任务应用增强,那么你不应该使用加载时 Weaver。 `LoadTimeWeaver`接口是 Spring 提供的类,它允许以特定的方式插入 JPA `classtransformer’实例,这取决于环境是 Web 容器还是应用服务器。将`ClassTransformers`通过[agent](https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html)挂钩通常效率不高。代理针对整个虚拟机工作,并检查加载的每个类,这在生产服务器环境中通常是不希望的。 Spring 为各种环境提供了许多`LoadTimeWeaver`实现,使得`ClassTransformer`实例仅适用于每个类装入器,而不适用于每个 VM。 有关`LoadTimeWeaver`实现及其设置的更多了解,请参见 AOP 章中的[Spring configuration](core.html#aop-aj-ltw-spring),这些实现及其设置可以是通用的,也可以是针对各种平台(例如 Tomcat、JBoss 和 WebSphere)定制的。 如[Spring configuration](core.html#aop-aj-ltw-spring)中所述,你可以通过使用`@EnableLoadTimeWeaving`注释或 `context:load-time-weaver`xml 元素来配置上下文范围的`LoadTimeWeaver`。所有 JPA `LocalContainerEntityManagerFactoryBean`实例都会自动拾取这样的全局织布器。下面的示例展示了设置加载时 Weaver、交付平台的自动检测(例如 Tomcat 的可编织的类装入器或 Spring 的 JVM 代理)以及将 Weaver 自动传播到所有可感知 Weaver 的 bean 的首选方法: ``` <context:load-time-weaver/> <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> ... </bean> ``` 但是,如果需要,你可以通过“loadTimeWeaver”属性手动指定一个专用的 Weaver,如下例所示: ``` <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="loadTimeWeaver"> <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/> </property> </bean> ``` 无论 LTW 如何配置,通过使用该技术, JPA 依赖于检测的应用程序可以在目标平台(例如 Tomcat)中运行,而不需要代理。当托管应用程序依赖于不同的实现方式时,这一点尤其重要,因为 JPA 变压器仅在类装入器级别上应用,并且因此彼此隔离。 ##### 处理多个持久性单元 对于依赖于多个持久性单元位置的应用程序(例如,存储在 Classpath 中的各种 JAR 中), Spring 提供了`PersistenceUnitManager`来充当中心存储库并避免持久性单元的发现过程,这可能是昂贵的。默认实现允许指定多个位置。这些位置会被解析,然后通过持久性单元名称检索。(默认情况下,在 Classpath 中搜索`META-INF/persistence.xml`文件。)以下示例配置了多个位置: ``` <bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager"> <property name="persistenceXmlLocations"> <list> <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value> <value>classpath:/my/package/**/custom-persistence.xml</value> <value>classpath*:META-INF/persistence.xml</value> </list> </property> <property name="dataSources"> <map> <entry key="localDataSource" value-ref="local-db"/> <entry key="remoteDataSource" value-ref="remote-db"/> </map> </property> <!-- if no datasource is specified, use this one --> <property name="defaultDataSource" ref="remoteDataSource"/> </bean> <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitManager" ref="pum"/> <property name="persistenceUnitName" value="myCustomUnit"/> </bean> ``` 默认的实现允许对`PersistenceUnitInfo`实例(在它们被馈送到 JPA 提供程序之前)进行定制,可以是声明式的(通过其影响所有托管单元的属性),也可以是程序化的(通过允许持久性单元选择的 `PersistenceUnitPostProcessor’)。如果没有指定“PersistenceUnitManager”,则由“LocalContaineRentyManagerFactoryBean”在内部创建和使用。 ##### 背景引导 `LocalContainerEntityManagerFactoryBean`支持通过`bootstrapExecutor`属性进行后台引导,如下例所示: ``` <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="bootstrapExecutor"> <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/> </property> </bean> ``` JPA 提供程序的实际引导将交给指定的执行器,然后并行地运行给应用程序引导线程。公开的`EntityManagerFactory`代理可以被注入到其他应用程序组件中,甚至能够响应“EntityManagerFactoryInfo”配置检查。然而,一旦实际的 JPA 提供程序被其他组件访问(例如,调用`createEntityManager`),这些调用就会阻塞,直到后台引导完成为止。特别是,当你使用 Spring 数据 JPA 时,请确保也为其存储库设置延迟引导。 #### 5.4.2.基于 JPA 实现 DAO:`EntityManagerFactory`和`EntityManager` | |虽然`EntityManagerFactory`实例是线程安全的,但`EntityManager`实例不是。注入的 JPA `EntityManager`的行为类似于从`EntityManager`应用服务器的 JNDI 环境中获取的`EntityManager`,如 JPA 规范所定义的。它将<br/>所有调用委托给当前事务`EntityManager`(如果有的话)。否则,它将返回<br/>到每个操作新创建的`EntityManager`,实际上使其使用线程安全。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 通过使用注入的`EntityManagerFactory`或`EntityManager`,可以在没有任何 Spring 依赖关系的情况下针对普通 JPA 编写代码。 Spring 如果启用了`PersistenceAnnotationBeanPostProcessor`,则可以在字段和方法级别上理解 @persistenceUnit 和`@PersistenceContext`注释。下面的示例显示了使用`@PersistenceUnit`注释的普通 JPA DAO 实现: Java ``` public class ProductDaoImpl implements ProductDao { private EntityManagerFactory emf; @PersistenceUnit public void setEntityManagerFactory(EntityManagerFactory emf) { this.emf = emf; } public Collection loadProductsByCategory(String category) { EntityManager em = this.emf.createEntityManager(); try { Query query = em.createQuery("from Product as p where p.category = ?1"); query.setParameter(1, category); return query.getResultList(); } finally { if (em != null) { em.close(); } } } } ``` Kotlin ``` class ProductDaoImpl : ProductDao { private lateinit var emf: EntityManagerFactory @PersistenceUnit fun setEntityManagerFactory(emf: EntityManagerFactory) { this.emf = emf } fun loadProductsByCategory(category: String): Collection<*> { val em = this.emf.createEntityManager() val query = em.createQuery("from Product as p where p.category = ?1"); query.setParameter(1, category); return query.resultList; } } ``` 前面的 DAO 对 Spring 没有依赖性,并且仍然很好地适合 Spring 应用程序上下文。此外,DAO 利用注释的优点来要求注入缺省的`EntityManagerFactory`,如下面的示例 Bean 中的定义所示: ``` <beans> <!-- bean post-processor for JPA annotations --> <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/> <bean id="myProductDao" class="product.ProductDaoImpl"/> </beans> ``` 作为显式定义`PersistenceAnnotationBeanPostProcessor`的替代方法,可以考虑在应用程序上下文配置中使用 Spring `context:annotation-config`XML 元素。这样做会自动注册用于基于注释的配置的所有 Spring 标准后处理程序,包括“CommonAnnotationBeanPostProcessor”等。 考虑以下示例: ``` <beans> <!-- post-processors for all standard config annotations --> <context:annotation-config/> <bean id="myProductDao" class="product.ProductDaoImpl"/> </beans> ``` 这种 DAO 的主要问题是,它总是通过工厂创建一个新的`EntityManager`。可以通过请求注入事务`EntityManager`(也称为“共享 EntityManager”,因为它是实际事务 EntityManager 的共享、线程安全代理)而不是工厂来避免这种情况。下面的示例展示了如何做到这一点: Java ``` public class ProductDaoImpl implements ProductDao { @PersistenceContext private EntityManager em; public Collection loadProductsByCategory(String category) { Query query = em.createQuery("from Product as p where p.category = :category"); query.setParameter("category", category); return query.getResultList(); } } ``` Kotlin ``` class ProductDaoImpl : ProductDao { @PersistenceContext private lateinit var em: EntityManager fun loadProductsByCategory(category: String): Collection<*> { val query = em.createQuery("from Product as p where p.category = :category") query.setParameter("category", category) return query.resultList } } ``` `@PersistenceContext`注释具有一个名为`type`的可选属性,该属性缺省为 `persistentecontextype.transaction’。你可以使用此默认值来接收共享的“EntityManager”代理。另一种选择,`PersistenceContextType.EXTENDED`,则是完全不同的事情。这导致了所谓的扩展`EntityManager`,它不是线程安全的,因此,不能在并发访问的组件中使用,例如 Spring 管理的单例 Bean。扩展的`EntityManager`实例只应该在有状态组件中使用,例如,这些组件驻留在会话中,而 `entityManager’的生命周期不绑定到当前事务,而是完全由应用程序决定。 方法和场级注入 你可以在类中的字段或方法上应用指示依赖注入的注释(例如`@PersistenceUnit`和 `@PersistenceConText`)——因此产生了表达式“方法级别的注入”和“字段级别的注入”。字段级别的注释简洁且易于使用,而方法级别的注释允许对注入的依赖项进行进一步处理。在这两种情况下,成员可见性(公共的、受保护的或私有的)并不重要。 那么类级别的注释呢? 在 Java EE 平台上,它们用于依赖性声明,而不是用于资源注入。 注入的`EntityManager`是 Spring-管理的(了解正在进行的事务)。尽管新的 DAO 实现使用了方法级注入的`EntityManager`而不是`EntityManagerFactory`,但由于注释的使用,不需要对应用程序上下文 XML 进行更改。 这种 DAO 风格的主要优点是它仅依赖于 Java 持久性 API。不需要输入任何 Spring 类。此外,如 JPA 注释所理解的,注入由 Spring 容器自动应用。从非侵犯性的角度来看,这是很有吸引力的,并且对于 JPA 开发人员来说可能会感觉更自然。 #### 5.4.3. Spring-驱动的 JPA 交易 | |如果你还没有<br/>,我们强烈建议你阅读[声明式事务管理](#transaction-declarative),以获得 Spring 的声明性事务支持的更详细的内容。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。 Spring 的`JpaTransactionManager`针对任何常规的 JDBC 连接池(不需要 XA),提供了从本地 JDBC 事务中已知的许多功能(例如特定于事务的隔离级别和资源级别的只读优化)。 Spring JPA 还允许配置的`JpaTransactionManager`将 JPA 事务公开到访问相同`DataSource`的 JDBC 访问代码,前提是注册的 `JPADianquence’支持对底层 JDBC`Connection`的检索。 Spring 为 EclipseLink 和 Hibernate JPA 实现方式提供了方言。有关`JpaDialect`机制的详细信息,请参见[next section](#orm-jpa-dialect)。 | |作为一种直接的替代方案, Spring 的本机`HibernateTransactionManager`能够<br/>与 JPA 访问代码进行交互,适应多个 Hibernate 细节并提供<br/>JDBC 交互。这与`LocalSessionFactoryBean`设置结合在一起特别有意义。详见[Native Hibernate Setup for JPA Interaction](#orm-jpa-hibernate)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.4.4.理解`JpaDialect`和`JpaVendorAdapter` 作为一项高级功能,`JpaTransactionManager`和 `AbstractEntityManagerFactoryBean’的子类允许将一个自定义`JpaDialect`传递到 `JPADianquence’ Bean 属性中。一个`JpaDialect`实现可以实现 Spring 支持的以下高级特性,通常是以特定于供应商的方式实现的: * 应用特定的事务语义(例如自定义隔离级别或事务超时) * 检索事务性 JDBC`Connection`(用于暴露于基于 JDBC 的 DAO) * 将`PersistenceExceptions`高级翻译为 Spring `DataAccessExceptions` 这对于特殊的事务语义和异常的高级转换特别有价值。默认实现不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。 | |作为主要针对 Spring 的全功能“LocalContaineRentyManagerFactoryBean”设置的更广泛的提供者适配工具,`JpaVendorAdapter`将<br/>的<br/>功能与其他提供者特定的默认值结合在一起。指定一个 HibernateJPavendorAdapter 或`EclipseLinkJpaVendorAdapter`是分别为 Hibernate 或 EclipseLink、<br/>自动配置`EntityManagerFactory`设置的最方便的<br/>方式。请注意,这些提供程序适配器主要设计用于<br/> Spring 驱动的事务管理(即,用于`JpaTransactionManager`)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 参见[`JpaDialect`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/JpaDialect.html)和[JPavendorAdapter](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/JpaVendorAdapter.html)Javadoc,以了解其操作的更多细节,以及在 Spring 的 JPA 支持中如何使用它们。 #### 5.4.5.使用 JTA 事务管理设置 JPA 作为`JpaTransactionManager`的替代方案, Spring 还允许通过 JTA 进行多资源事务协调,可以是在 Java EE 环境中,也可以是使用独立的事务协调器,例如 Atomikos。除了选择 Spring 的“jtaTransactionManager”而不是`JpaTransactionManager`,你还需要采取一些进一步的步骤: * 底层的 JDBC 连接池需要具有 XA 功能,并与事务协调器集成在一起。在 Java EE 环境中,这通常很简单,通过 JNDI 公开另一种`DataSource`。有关详细信息,请参阅你的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成`DataSource`变体。再次,检查其文档。 * 需要为 JTA 配置 JPA `EntityManagerFactory`设置。这是特定于提供者的,通常通过在`LocalContainerEntityManagerFactoryBean`上指定为`jpaProperties`的特殊属性。在 Hibernate 的情况下,这些属性甚至是版本特定的。有关详细信息,请参阅你的 Hibernate 文档。 * Spring 的`HibernateJpaVendorAdapter`强制执行某些面向 Spring 的默认值,例如连接释放模式,`on-close`,该模式与 Hibernate 自己在 Hibernate 5.0 中的默认值相匹配,但在 Hibernate 5.1+ 中不再匹配。对于 JTA 设置,请确保将你的持久性单元事务类型声明为“JTA”。或者,将 Hibernate 5.2 的 ` Hibernate.connection.handling_mode` 属性设置为 `delayed_acquisition_and_release_after_statement’,以恢复 Hibernate 自己的默认值。相关注解见[Spurious Application Server Warnings with Hibernate](#orm-hibernate-invalid-jdbc-access-error)。 * 或者,考虑从应用程序服务器本身获得`EntityManagerFactory`(即通过 JNDI 查找,而不是本地声明的 localContainerentyManagerFactoryBean`)。服务器提供的`EntityManagerFactory`在你的服务器配置中可能需要特殊的定义(使得部署不那么可移植),但它是为服务器的 JTA 环境设置的。 #### 5.4.6.用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务 结合`HibernateTransactionManager`的本机`LocalSessionFactoryBean`设置允许与`@PersistenceContext`和其他 JPA 访问代码进行交互。一个 Hibernate `SessionFactory’本地实现了 JPA 的`EntityManagerFactory`接口,而一个 Hibernate `Session`句柄本地实现了一个 JPA `EntityManager`。 Spring 的 JPA 支持设施自动检测本机 Hibernate 会话。 因此,在许多场景中,这样的本机 Hibernate 设置可以替代标准的 JPA `localContaineRentyManagerFactoryBean’和<gtR="1957"/>组合,允许在相同的本地事务中与<gtR="1958"/>(以及<gtR="1959"/>)旁边的<gtR="1960"/>进行交互。这样的设置还提供了更强的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导契约的约束。 在这样的场景中,你不需要`HibernateJpaVendorAdapter`配置,因为 Spring 的本机 Hibernate 设置提供了更多的功能(例如,自定义 Hibernate Integrator 设置、 Hibernate 5.3 Bean 容器集成,以及针对只读事务的更强优化)。最后但并非最不重要的是,你还可以通过`LocalSessionFactoryBuilder`表示本机 Hibernate 设置,与`@Bean`样式配置无缝集成(不涉及`FactoryBean`)。 | |`LocalSessionFactoryBean`和`LocalSessionFactoryBuilder`支持背景<br/>引导,就像 JPA `LocalContainerEntityManagerFactoryBean`所做的那样。<br/>参见[背景引导](#orm-jpa-setup-background)介绍。<br/><br/>on`LocalSessionFactoryBean`,这可以通过`bootstrapExecutor`属性获得。在程序化的`LocalSessionFactoryBuilder`上,重载的 `buildsessionFactory’方法需要一个引导程序执行器参数。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## 6. 使用对象-XML 映射器对 XML 进行编组 ### 6.1.导言 本章介绍 Spring 的对象-XML 映射支持。对象-XML 映射(Object-XML mapping,简称 O-X 映射)是将 XML 文档转换为对象和从对象转换的行为。这种转换过程也称为 XML 编组,或 XML 序列化。本章交替使用这些术语。 在 O-X 映射领域中,编组器负责将对象(图)序列化为 XML。以类似的方式,反编组器将 XML 反序列化为一个对象图。这个 XML 可以采取 DOM 文档、输入或输出流或 SAX 处理程序的形式。 针对你的 O/X 映射需求使用 Spring 的一些好处是: * [易于配置](#oxm-ease-of-configuration) * [一致的接口](#oxm-consistent-interfaces) * [一致的异常层次结构](#oxm-consistent-exception-hierarchy) #### 6.1.1.易于配置 Spring 的 Bean 工厂使得很容易配置编组器,而不需要构建 JAXB 上下文、JiBX 绑定工厂等。你可以像在你的应用程序上下文中的任何其他 Bean 一样配置编组器。此外,基于 XML 名称空间的配置可用于许多编组器,这使得配置更加简单。 #### 6.1.2.一致的接口 Spring 的 O-X 映射通过两个全局接口进行操作:[`Marshaller`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/Marshaller.html)和[`Unmarshaller`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/Unmarshaller.html)。这些抽象允许你相对轻松地切换 O-X 映射框架,只需对进行编组的类进行很少或根本不需要更改。这种方法还有一个额外的好处,那就是可以使用混合匹配的方法(例如,使用 JAXB 和 XStream 执行一些编组)以非侵入性的方式进行 XML 编组,从而使你能够利用每种技术的优势。 #### 6.1.3.一致的异常层次结构 Spring 提供了从底层 O-X 映射工具的异常到其自身的异常层次结构的转换,并将`XmlMappingException`作为根异常。这些运行时异常会对原始异常进行包装,这样就不会丢失任何信息。 ### 6.2.`Marshaller`和`Unmarshaller` 正如[introduction](#oxm-introduction)中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象。本节描述了用于此目的的两个 Spring 接口。 #### 6.2.1.理解`Marshaller` Spring 抽象了 `org.SpringFramework.OXM.Marshaller’接口背后的所有编组操作,其主要方法如下: ``` public interface Marshaller { /** * Marshal the object graph with the given root into the provided Result. */ void marshal(Object graph, Result result) throws XmlMappingException, IOException; } ``` `Marshaller`接口有一个主要方法,它将给定对象封送到给定的`javax.xml.transform.Result`。其结果是一个标记接口,它基本上表示一个 XML 输出抽象。具体的实现封装了各种 XML 表示,如下表所示: |Result implementation|包装 XML 表示| |---------------------|-----------------------------------------------------------| | `DOMResult` |`org.w3c.dom.Node`| | `SAXResult` |`org.xml.sax.ContentHandler`| | `StreamResult` |`java.io.File`,`java.io.OutputStream`,或`java.io.Writer`| | |尽管`marshal()`方法接受一个普通对象作为其第一个参数,但大多数 `Marshaller’实现不能处理任意对象。相反,对象类<br/>必须映射到映射文件中,用注释标记,用<br/>编组器注册,或者有一个公共的基类。请参阅本章<br/>中后面的部分,以确定你的 O-X 技术如何管理这一点。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.2.2.理解`Unmarshaller` 与`Marshaller`类似,我们有`org.springframework.oxm.Unmarshaller`接口,下面的列表显示了该接口: ``` public interface Unmarshaller { /** * Unmarshal the given provided Source into an object graph. */ Object unmarshal(Source source) throws XmlMappingException, IOException; } ``` 这个接口也有一个方法,它从给定的“javax.xml.transform.source”(一种 XML 输入抽象)读取对象,并返回读取的对象。与`Result`一样,`Source`是一个标记接口,具有三个具体的实现。每一种都包装了不同的 XML 表示形式,如下表所示: |Source implementation|包装 XML 表示| |---------------------|----------------------------------------------------------| | `DOMSource` |`org.w3c.dom.Node`| | `SAXSource` |`org.xml.sax.InputSource`,和`org.xml.sax.XMLReader`| | `StreamSource` |`java.io.File`,`java.io.InputStream`,or`java.io.Reader`| 即使有两个独立的编组接口(“Marshaller”和“Unmarshaller”), Spring-WS 中的所有实现都在一个类中实现这两个接口。这意味着你可以连接一个 Marshaller 类,并在`applicationContext.xml`中同时将其称为一个 Marshaller 和一个 Unshaller。 #### 6.2.3.理解`XmlMappingException` Spring 将来自底层 O-X 映射工具的异常转换为其自身的异常层次结构,并将`XmlMappingException`作为根异常。这些运行时异常将包装原始异常,这样就不会丢失任何信息。 此外,`MarshallingFailureException`和`UnmarshallingFailureException`提供了编组和解组操作之间的区别,即使底层的 O-X 映射工具不这样做。 O-X 映射异常层次结构如下图所示: ![oxm exceptions](images/oxm-exceptions.png) ### 6.3.使用`Marshaller`和`Unmarshaller` 你可以在各种各样的情况下使用 Spring 的 OXM。在下面的示例中,我们使用它将 Spring 管理的应用程序的设置编组为 XML 文件。在下面的示例中,我们使用一个简单的 JavaBean 来表示设置: Java ``` public class Settings { private boolean fooEnabled; public boolean isFooEnabled() { return fooEnabled; } public void setFooEnabled(boolean fooEnabled) { this.fooEnabled = fooEnabled; } } ``` Kotlin ``` class Settings { var isFooEnabled: Boolean = false } ``` 应用程序类使用此 Bean 来存储其设置。除了一个主方法之外,这个类还有两个方法:`saveSettings()`将设置保存到一个名为 `settings.xml’的文件中,`loadSettings()`再次加载这些设置。下面的`main()`方法构造了一个 Spring 应用程序上下文,并调用这两个方法: Java ``` import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; public class Application { private static final String FILE_NAME = "settings.xml"; private Settings settings = new Settings(); private Marshaller marshaller; private Unmarshaller unmarshaller; public void setMarshaller(Marshaller marshaller) { this.marshaller = marshaller; } public void setUnmarshaller(Unmarshaller unmarshaller) { this.unmarshaller = unmarshaller; } public void saveSettings() throws IOException { try (FileOutputStream os = new FileOutputStream(FILE_NAME)) { this.marshaller.marshal(settings, new StreamResult(os)); } } public void loadSettings() throws IOException { try (FileInputStream is = new FileInputStream(FILE_NAME)) { this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is)); } } public static void main(String[] args) throws IOException { ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Application application = (Application) appContext.getBean("application"); application.saveSettings(); application.loadSettings(); } } ``` Kotlin ``` class Application { lateinit var marshaller: Marshaller lateinit var unmarshaller: Unmarshaller fun saveSettings() { FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) } } fun loadSettings() { FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings } } } private const val FILE_NAME = "settings.xml" fun main(args: Array<String>) { val appContext = ClassPathXmlApplicationContext("applicationContext.xml") val application = appContext.getBean("application") as Application application.saveSettings() application.loadSettings() } ``` `Application`需要同时设置`marshaller`和`unmarshaller`属性。我们可以通过使用以下`applicationContext.xml`来做到这一点: ``` <beans> <bean id="application" class="Application"> <property name="marshaller" ref="xstreamMarshaller" /> <property name="unmarshaller" ref="xstreamMarshaller" /> </bean> <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/> </beans> ``` 这个应用程序上下文使用 XStream,但我们可以使用本章后面描述的任何其他 Marshaller 实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 Bean 的定义相当简单。还要注意,“xStreamMarshaller”同时实现了`Marshaller`和`Unmarshaller`,因此我们可以在应用程序的`marshaller`和`unmarshaller`属性中引用“xStreamMarshaller" Bean。 此示例应用程序生成以下`settings.xml`文件: ``` <?xml version="1.0" encoding="UTF-8"?> <settings foo-enabled="false"/> ``` ### 6.4.XML 配置命名空间 通过使用 OXM 名称空间中的标记,可以更简洁地配置编组器。要使这些标记可用,你必须首先在 XML 配置文件的前导码中引用适当的模式。下面的示例展示了如何做到这一点: ``` <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:oxm="http://www.springframework.org/schema/oxm" (1) xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2) ``` |**1**|引用`oxm`模式。| |-----|----------------------------------| |**2**|指定`oxm`模式位置。| 该模式使以下元素可用: * [JAXB2-编组器](#oxm-jaxb2-xsd) * [JiBX-Marshaller’](#oxm-jibx-xsd) 每个标记在其各自的编组器部分中进行了解释。但是,作为示例,JAXB2 封送编组器的配置可能类似于以下内容: ``` <oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/> ``` ### 6.5.JAXB JAXB 绑定编译器将 W3CXML 模式转换为一个或多个 Java 类、一个“jaxb.properties”文件,可能还有一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。 Spring 支持 JAXB2.0API 作为 XML 编组策略,遵循[`Marshaller` and `Unmarshaller`](#oxm-marshaller-unmarshaller)中描述的 `marshaller’和`Unmarshaller`接口。相应的集成类驻留在`org.springframework.oxm.jaxb`包中。 #### 6.5.1.使用`Jaxb2Marshaller` `Jaxb2Marshaller`类实现了 Spring 的`Marshaller`和`Unmarshaller`接口。它需要一个上下文路径来操作。你可以通过设置“ContextPath”属性来设置上下文路径。上下文路径是包含模式派生类的冒号分隔的 Java 包名称的列表。它还提供了一个`classesToBeBound`属性,它允许你设置一个由 Marshaller 支持的类数组。模式验证是通过向 Bean 指定一个或多个模式资源来执行的,如以下示例所示: ``` <beans> <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>org.springframework.oxm.jaxb.Flight</value> <value>org.springframework.oxm.jaxb.Flights</value> </list> </property> <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/> </bean> ... </beans> ``` ##### XML 配置命名空间 `jaxb2-marshaller`元素配置了`org.springframework.oxm.jaxb.Jaxb2Marshaller`,如下例所示: ``` <oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/> ``` 或者,你可以通过使用“类-be-bound”子元素来提供要绑定到编组器的类的列表: ``` <oxm:jaxb2-marshaller id="marshaller"> <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/> <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/> ... </oxm:jaxb2-marshaller> ``` 下表描述了可用的属性: | Attribute |说明|Required| |-------------|------------------------|--------| | `id` |编组员的身份| No | |`contextPath`|JAXB 上下文路径| No | ### 6.6.JiBX JiBX 框架提供了一种类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了如何将 Java 对象转换为 XML 或从 XML 转换的规则。在准备好绑定和编译类之后,JiBX 绑定编译器将增强类文件,并添加代码来处理将类的实例从 XML 转换为 XML。 有关 JiBX 的更多信息,请参见[JiBX web site](http://jibx.sourceforge.net/)。 Spring 集成类驻留在`org.springframework.oxm.jibx`包中。 #### 6.6.1.使用`JibxMarshaller` `JibxMarshaller`类实现了`Marshaller`和`Unmarshaller`接口。要进行操作,它需要封送一个类的名称,你可以使用`targetClass`属性对其进行设置。也可以通过设置“bindingname”属性来设置绑定名称。在下面的示例中,我们绑定`Flights`类: ``` <beans> <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller"> <property name="targetClass">org.springframework.oxm.jibx.Flights</property> </bean> ... </beans> ``` 为单个类配置了`JibxMarshaller`。如果要封送多个类,则必须配置具有不同`JibxMarshaller`属性值的多个`targetClass`实例。 ##### XML 配置命名空间 `jibx-marshaller`标记配置了`org.springframework.oxm.jibx.JibxMarshaller`,如下例所示: ``` <oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/> ``` 下表描述了可用的属性: | Attribute |说明|Required| |--------------|----------------------------------------|--------| | `id` |编组员的身份| No | |`target-class`|这个编组器的目标类| Yes | |`bindingName` |这个编组器使用的绑定名称| No | ### 6.7.XStream XStream 是一个简单的库,用于将对象序列化为 XML,然后再将对象序列化。它不需要任何映射,并生成干净的 XML。 有关 XStream 的更多信息,请参见[XStream 网站](https://x-stream.github.io/)。 Spring 集成类驻留在 `org.SpringFramework.OXM.xStream’包中。 #### 6.7.1.使用`XStreamMarshaller` `XStreamMarshaller`不需要任何配置,可以直接在应用程序上下文中进行配置。为了进一步定制 XML,你可以设置一个别名映射,它由映射到类的字符串别名组成,如下例所示: ``` <beans> <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="aliases"> <props> <prop key="Flight">org.springframework.oxm.xstream.Flight</prop> </props> </property> </bean> ... </beans> ``` | |默认情况下,XStream 允许对任意类进行解编,这可能导致<br/>不安全的 Java 序列化效果。因此,我们不建议使用“XStreamMarshaller”来从外部来源(即 Web)分解 XML,因为这可能会<br/>导致安全漏洞。<br/><br/>如果你选择使用`XStreamMarshaller`从外部源解编 XML,<br/>在`XStreamMarshaller`上设置`supportedClasses`属性,如下例所示:<br/><br/>``<br/><bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"><br/><property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/><br/></bean><br/>``<<<<2079"/>这样做可确保只有已注册的类才有资格进行 unmarshalling。<81"/>”R=“2081”/>“R=”,“R=”=“2082”R=“的类只能确保你另外注册的类得到支持,”gt=“R=”<20 除了显式支持应该支持的域类的<br/>转换器之外,你可能希望在列表中添加一个`CatchAllConverter`作为最后一个转换器。作为<br/>结果,不会调用具有较低优先级和可能的安全性<br/>漏洞的默认 XStream 转换器。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |请注意,XStream 是一个 XML 序列化库,而不是一个数据绑定库。<br/>因此,它对名称空间的支持有限。因此,它非常不适合在 Web 服务中使用<br/>。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## 7. 附录 ### 7.1.XML 模式 附录的这一部分列出了用于数据访问的 XML 模式,包括以下内容: * [The `tx` Schema](#xsd-schemas-tx) * [The `jdbc` Schema](#xsd-schemas-jdbc) #### 7.1.1.`tx`模式 `tx`标记处理在 Spring 的事务全面支持中配置所有这些 bean。这些标记在标题为[事务管理](#transaction)的章节中进行了介绍。 | |我们强烈建议你查看带有<br/> Spring 发行版的`'spring-tx.xsd'`文件。该文件包含 Spring 事务<br/>配置的 XML 模式,并涵盖`tx`命名空间中的所有不同元素,包括<br/>属性默认值和类似信息。这个文件是内联文档化的,因此,<br/>这里不重复信息是为了遵守 dry(不要<br/>重复自己)原则。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 为了完整起见,要使用`tx`模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此`tx`名称空间中的标记对你是可用的: ``` <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" (1) xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd (2) http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- bean definitions here --> </beans> ``` |**1**|声明`tx`名称空间的用法。| |-----|---------------------------------------------------| |**2**|指定位置(与其他架构位置一起)。| | |通常,当你使用`tx`命名空间中的元素时,你还使用<br/>命名空间中的<br/>元素(因为 Spring 中的声明性事务支持是通过使用 AOP 实现的<br/>)。前面的 XML 片段包含引用<br/>模式所需的相关行,以便`aop`名称空间中的元素对你可用<br/>。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 7.1.2.`jdbc`模式 `jdbc`元素允许你快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在[嵌入式数据库支持](#jdbc-embedded-database-support)和[初始化数据源](#jdbc-initializing-datasource)中。 要使用`jdbc`模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此`jdbc`名称空间中的元素对你是可用的: ``` <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" (1) xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2) <!-- bean definitions here --> </beans> ```