namalzi

← All posts

Digging into Spring's Source 19: ORM and R2DBC Integration — JPA Support and Reactive Transactions

Spring · Published

How JPA's EntityManagerFactory becomes a bean, why the EntityManager you get from @PersistenceContext is safe to share between threads, how JdbcTemplate ends up on the same connection under JpaTransactionManager, and where a @Repository's exceptions get translated — all checked in the source. Then the reactive side, where there is no thread to hang a transaction on, and R2DBC transactions ride in the Reactor Context instead.

What this post covers

This is the nineteenth post in the "Digging into Spring's Source" series. Part 17 looked at who opens and closes a transaction (PlatformTransactionManager, the @Transactional proxy, and TransactionSynchronizationManager, which ties resources to the thread), and Part 18 showed JdbcTemplate finding that thread-bound connection through DataSourceUtils. This time we see two things built on top of that frame: JPA (an ORM such as Hibernate), and reactive data access (R2DBC), where there is no thread at all. With this post, Phase 4 (transactions and data access) of the map in Part 1 is complete.

There are four questions to answer.

Item Details
Source Spring Framework 7.1.0-SNAPSHOT
Commit f447f3c310 (2026-10-01, main branch)
Modules read most spring-orm, spring-r2dbc, spring-tx (transaction.reactive, dao.support, dao.annotation)
Classes read most LocalContainerEntityManagerFactoryBean, SharedEntityManagerCreator, JpaTransactionManager, PersistenceExceptionTranslationInterceptor, R2dbcTransactionManager, TransactionContextManager

Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.

My study notes go much deeper. This post keeps only the main thread that someone seeing Spring for the first time can follow. What was left out is named at the end, under "If you want to go deeper".

Words to know first

JPA (Jakarta Persistence API) is the standard interface for mapping Java objects to database tables (ORM, object-relational mapping). It is only an interface, so an implementation has to do the real work; the examples in this post use Hibernate. JPA has two central objects. An EntityManagerFactory is the heavyweight factory you create once, and an EntityManager is the lightweight working handle it produces, with methods such as persist (save) and find (load). JPA's rule is that an EntityManager is not thread-safe.

A persistence unit is a bundle of settings: "for this database, use these entity classes". Traditionally it lives in a META-INF/persistence.xml file, but Spring can do without the file if you just name the packages.

A FactoryBean is a bean that goes into the container but exposes another object it builds as the actual bean — a small factory registered as a bean. Part 3, section 6 covered it. The EntityManagerFactory in this post becomes a bean that way.

Transaction managers and thread-bound resources. Two things from Part 17 to keep in mind. PlatformTransactionManager is the interface that opens (getTransaction) and closes (commit/rollback) a transaction, and its skeleton implementation AbstractPlatformTransactionManager decides whether to open a new one and leaves how to abstract methods such as doBegin (lines 524–532, line 1159). The resources of an open transaction, such as its connection, are kept by TransactionSynchronizationManager in a per-thread store (ThreadLocal) as "key → resource" pairs (lines 75–78). The key is usually the resource factory, such as a DataSource.

Reactive streams and Reactor. Instead of returning a result right away, a method returns "a pipe that values will flow through later" (Mono for 0–1 values, Flux for many), and the real work happens only when somebody subscribes. One piece of work may hop across several threads, which makes ThreadLocal useless. Reactor instead has a small map called the Context that travels with each subscription. R2DBC is JDBC's reactive counterpart: you still get a Connection from a ConnectionFactory, but every method returns a Mono or a Flux.

1. How EntityManagerFactory becomes a bean

A JPA configuration usually looks like this. The essentials from JpaConfig in the example folder ex/u17/:

// An example written for this post (not Spring's code)
@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(dataSource);
    emf.setPackagesToScan("ex.u17");           // scan a package for @Entity instead of persistence.xml
    HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
    adapter.setGenerateDdl(true);              // hibernate.hbm2ddl.auto=update → creates the tables
    adapter.setShowSql(false);                 // turn off SQL logging
    emf.setJpaVendorAdapter(adapter);
    return emf;
}

@Bean
PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
}

The @Bean method returns a LocalContainerEntityManagerFactoryBean, yet the second method is injected with an EntityManagerFactory. How do two different types connect? Because the first class is a FactoryBean.

public abstract class AbstractEntityManagerFactoryBean implements
		SmartFactoryBean<EntityManagerFactory>, BeanClassLoaderAware, BeanFactoryAware,
		BeanNameAware, InitializingBean, SmartInitializingSingleton, DisposableBean,
		EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable {

spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java, lines 98–101

This one declaration holds everything in this section and in section 4. Being a SmartFactoryBean<EntityManagerFactory>, the bean that goes into the container is not this object but the EntityManagerFactory returned by getObject() (lines 655–657). Being an InitializingBean, afterPropertiesSet() is called once the properties are set, and that is where the factory is actually built. PersistenceExceptionTranslator is used for exception translation in section 4.

In LocalContainerEntityManagerFactoryBean.afterPropertiesSet, DefaultPersistenceUnitManager either reads persistence.xml or scans packagesToScan for entities to build a PersistenceUnitInfo. AbstractEntityManagerFactoryBean.afterPropertiesSet then takes the provider, properties and dialect from the JpaVendorAdapter, builds Hibernate's EntityManagerFactory in createNativeEntityManagerFactory, wraps it in createEntityManagerFactoryProxy, and getObject exposes that proxy as the bean. How the factory gets built. Blue boxes are Spring's procedure, grey is our configuration and what Hibernate produced, green is the bean that enters the container.

The persistence unit — why persistence.xml is optional

LocalContainerEntityManagerFactoryBean holds a DefaultPersistenceUnitManager inside (line 102), and setPackagesToScan simply passes through to it (lines 223–225). afterPropertiesSet() first prepares that manager and then determines the persistence unit info (lines 384–391). This is where the manager reads it.

	private List<SpringPersistenceUnitInfo> readPersistenceUnitInfos() {
		List<SpringPersistenceUnitInfo> infos = new ArrayList<>(1);
		String defaultName = this.defaultPersistenceUnitName;
		boolean buildDefaultUnit = (this.persistenceConfiguration != null || this.managedTypes != null ||
				this.packagesToScan != null || this.mappingResources != null);
		boolean foundDefaultUnit = false;

		PersistenceUnitReader reader = new PersistenceUnitReader(this.resourcePatternResolver, this.dataSourceLookup);
		SpringPersistenceUnitInfo[] readInfos = reader.readPersistenceUnitInfos(this.persistenceXmlLocations);
		// ...
		if (buildDefaultUnit) {
			// ...
				infos.add(buildDefaultPersistenceUnitInfo());
			// ...
		}
		return infos;
	}

spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java, lines 522–551

A persistence.xml is read if it exists at the default location (classpath*:META-INF/persistence.xml, line 91), and it is fine if it does not. When packagesToScan is set, buildDefaultUnit is true and Spring builds the default unit itself. Inside that method a PersistenceManagedTypesScanner walks the packages and collects the @Entity classes (lines 557–598, the scanner at lines 572–574). The JPA standard demands an XML file; Spring does the file's job in code instead.

The vendor adapter — where the Hibernate settings come together

AbstractEntityManagerFactoryBean.afterPropertiesSet() takes three things from the JpaVendorAdapter, if there is one.

	@Override
	public void afterPropertiesSet() throws PersistenceException {
		JpaVendorAdapter jpaVendorAdapter = getJpaVendorAdapter();
		if (jpaVendorAdapter != null) {
			if (this.persistenceProvider == null) {
				this.persistenceProvider = jpaVendorAdapter.getPersistenceProvider();
			}
			PersistenceUnitInfo pui = getPersistenceUnitInfo();
			Map<String, ?> vendorPropertyMap = (pui != null ? jpaVendorAdapter.getJpaPropertyMap(pui) :
					jpaVendorAdapter.getJpaPropertyMap());
			// ...
			if (this.jpaDialect == null) {
				this.jpaDialect = jpaVendorAdapter.getJpaDialect();
			}
		}

		// ...
			this.nativeEntityManagerFactory = buildNativeEntityManagerFactory();
		// ...
		// Wrap the EntityManagerFactory in a factory implementing all its interfaces.
		// This allows interception of createEntityManager methods to return an
		// application-managed EntityManager proxy that automatically joins
		// existing transactions.
		this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory);

spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java, lines 372–423

Then buildNativeEntityManagerFactory() calls the abstract createNativeEntityManagerFactory() (lines 437–440, line 566), and the implementation in LocalContainerEntityManagerFactoryBean hands the persistence unit info and the property map to the provider and gets the real factory back (lines 423–447, the call at lines 442–443). This is where Hibernate reads the entity classes and creates the tables.

The bean is a proxy

The last line matters. The factory that came back is not exposed as it is; createEntityManagerFactoryProxy() wraps it once more.

	protected EntityManagerFactory createEntityManagerFactoryProxy(@Nullable EntityManagerFactory emf) {
		Set<Class<?>> ifcs = new LinkedHashSet<>();
		Class<?> entityManagerFactoryInterface = this.entityManagerFactoryInterface;
		if (entityManagerFactoryInterface != null) {
			ifcs.add(entityManagerFactoryInterface);
		}
		// ...
		ifcs.add(EntityManagerFactoryInfo.class);

		try {
			return (EntityManagerFactory) Proxy.newProxyInstance(this.beanClassLoader,
					ClassUtils.toClassArray(ifcs), new ManagedEntityManagerFactoryInvocationHandler(this));
		}

spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java, lines 480–497

This is the JDK dynamic proxy from Part 10. One extra interface goes into the list, EntityManagerFactoryInfo, and through it other Spring code (the transaction manager, the shared proxy) can ask "which DataSource does this factory use?", "what is the JpaDialect?", "what is the native factory?" (EntityManagerFactoryInfo line 75, line 100, line 112). As the source comment above says, it also lets Spring intercept createEntityManager calls.

Easy to get wrong

2. Why the @PersistenceContext EntityManager is thread-safe

By JPA's rules an EntityManager is not thread-safe. Yet in Spring you put one EntityManager into a field of a singleton bean and let every request thread use it. The answer is that what goes into the field is not a real EntityManager but a shared proxy.

Who injects it — PersistenceAnnotationBeanPostProcessor

@PersistenceContext is handled by one of the BeanPostProcessors we met in Part 6.

public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor,
		DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor,
		PriorityOrdered, BeanFactoryAware, Serializable {

spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java, lines 189–191

You do not register it yourself. AnnotationConfigUtils.registerAnnotationConfigProcessors, seen in Part 8, registers this post-processor too whenever JPA is on the classpath (that is, when jakarta.persistence.EntityManagerFactory can be loaded) (lines 122–124, lines 177–189). It works just like the AutowiredAnnotationBeanPostProcessor of Part 5: postProcessProperties finds the list of injection points and tells it to inject (lines 399–409), and the place where one injection point decides "what to inject" is getResourceToInject (lines 758–771). For @PersistenceUnit it injects the factory; for @PersistenceContext an EntityManager, and the default path for the latter is this.

		private EntityManager resolveEntityManager(@Nullable String requestingBeanName) {
			// Obtain EntityManager reference from JNDI?
			EntityManager em = getPersistenceContext(this.unitName, false);
			if (em == null) {
				// ...
					emf = findEntityManagerFactory(this.unitName, requestingBeanName);
				// ...
				// Inject a shared transactional EntityManager proxy.
				if (emf instanceof EntityManagerFactoryInfo emfInfo && emfInfo.getEntityManagerInterface() != null) {
					// Create EntityManager based on the info's vendor-specific type
					// (which might be more specific than the field's type).
					em = SharedEntityManagerCreator.createSharedEntityManager(
							emf, this.properties, this.synchronizedWithTransaction);
				}
				// ...
			}
			return em;
		}

spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java, lines 783–808

The comment gives the answer: "Inject a shared transactional EntityManager proxy." The one building it is SharedEntityManagerCreator, and it is a JDK proxy again (lines 165–178). One more interface, EntityManagerProxy, is appended to the list (line 174); its getTargetEntityManager() lets you pull out "the real object behind this proxy right now" (EntityManagerProxy line 44). The example uses it.

On every call the proxy looks up "the EntityManager of the current transaction"

Every method call on the proxy comes in through one invoke. After a few specially handled methods, this is the main part.

		@Override
		public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
			// Invocation on EntityManager interface coming in...

			switch (method.getName()) {
				// ...
				case "close" -> {
					// Handle close method: suppress, not valid.
					return null;
				}
				case "getTransaction" -> {
					throw new IllegalStateException(
							"Not allowed to create transaction on shared EntityManager - " +
							"use Spring transactions or EJB CMT instead");
				}
			}

			// Determine current EntityManager: either the transactional one
			// managed by the factory or a temporary one for the given invocation.
			EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
					this.targetFactory, this.properties, this.synchronizedWithTransaction);
			// ...
			if (transactionRequiringMethods.contains(method.getName())) {
				// We need a transactional target now, according to the JPA spec.
				// Otherwise, the operation would get accepted but remain unflushed...
				if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() &&
						!target.getTransaction().isActive())) {
					throw new TransactionRequiredException("No EntityManager with actual transaction available " +
							"for current thread - cannot reliably process '" + method.getName() + "' call");
				}
			}

			// Regular EntityManager operations.
			boolean newTarget = false;
			if (target == null) {
				// ...
				target = EntityManagerFactoryUtils.createEntityManager(this.targetFactory, this.properties);
				newTarget = true;
			}
			// ...
			return invokeMethod(method, args, target, newTarget);
		}

spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java, lines 316–414

A shared proxy goes into the @PersistenceContext field. On every call, invoke uses EntityManagerFactoryUtils.doGetTransactionalEntityManager to look for the EntityManagerHolder bound to the thread. If there is one, the call runs on that transaction's EntityManager; if not, transaction-requiring methods such as persist throw TransactionRequiredException, and any other method gets an EntityManager created for this one call and closed afterwards. What the proxy does on each call. The yellow box is where it looks into the thread store from Part 17.

Reading from the top:

  1. close() is ignored and getTransaction() is refused. If one user closed a shared object or opened a transaction on it directly, every other user would break. Transactions are Spring's job.
  2. doGetTransactionalEntityManager looks for "the EntityManager bound to the current thread's transaction". It calls TransactionSynchronizationManager.getResource(emf) — the thread store from Part 17, keyed by the factory (EntityManagerFactoryUtils line 207). If an EntityManagerHolder is bound, the EntityManager inside it is returned (line 231); if transaction synchronization is not even active, it returns null (lines 253–256).
  3. persist, merge, remove, flush and friends throw TransactionRequiredException when there is no transaction. The list is at lines 76–82. The comment gives a good reason: accepting the call would leave the change silently unflushed.
  4. Any other method (find, for example) gets a fresh EntityManager made for this one call when there is no transaction, closed when the call is over (the finally in invokeMethod, lines 287–289).

That is why it is thread-safe. The proxy itself holds no state; the real EntityManager exists per thread and per transaction, and the proxy just finds it on every call. It is the same shape as JdbcTemplate in Part 18 finding the thread-bound connection through DataSourceUtils.getConnection. The one that binds the EntityManagerHolder is the JpaTransactionManager of section 3.

Example: outside and inside a transaction

This is ex/u17/SharedEntityManagerExample.java. The example was run with the released Spring Framework 7.0.9 and Hibernate 7.1.36 to confirm the output below (the 7.1.0-SNAPSHOT this series reads was not built and run directly).

// An example written for this post (not Spring's code)
@Component
static class MemberDao {
    @PersistenceContext
    EntityManager em;                 // injected by PersistenceAnnotationBeanPostProcessor

    @Autowired
    EntityManager autowiredEm;        // since 7.0: the FactoryBean hands it out via SmartFactoryBean.getObject(Class)
}
// ... in main: outside a transaction
System.out.println("em is JDK proxy      : " + Proxy.isProxyClass(dao.em.getClass()));
// ...
try {
    ((EntityManagerProxy) dao.em).getTargetEntityManager();
}
catch (IllegalStateException ex) {
    System.out.println("getTargetEntityManager : " + ex.getMessage());
}
try {
    dao.em.persist(new Member("kim"));
}
catch (TransactionRequiredException ex) {
    System.out.println("persist                : " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
}
// reading works — an EntityManager is created for this one call and closed again
System.out.println("find(1L)               : " + dao.em.find(Member.class, 1L));
// ... inside a transaction
TransactionTemplate tx = new TransactionTemplate(ctx.getBean(PlatformTransactionManager.class));
tx.executeWithoutResult(status -> {
    // ...
    EntityManager t1 = ((EntityManagerProxy) dao.em).getTargetEntityManager();
    EntityManager t2 = ((EntityManagerProxy) dao.autowiredEm).getTargetEntityManager();
    System.out.println("same target for both fields? " + (t1 == t2));
    dao.em.persist(new Member("kim"));
    // ...
});
em is JDK proxy      : true
em.toString()        : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@185339ed]
em is EntityManagerProxy : true
@Autowired em        : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@185339ed]
emf is JDK proxy     : true
--- outside transaction
EntityManagerHolder bound to thread? false
getTargetEntityManager : No transactional EntityManager available
persist                : TransactionRequiredException - No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
find(1L)               : null
--- inside transaction
EntityManagerHolder bound to thread? true
same target for both fields? true
persist                : ok
after commit, find(1L) : Member[1, kim]

Outside a transaction the store is empty (false), asking for the real object fails, persist is refused, but find works. Inside a transaction the store holds an EntityManagerHolder, and the proxies in both fields point at the same real object.

Easy to get wrong: @PersistenceContext versus @Autowired EntityManager

In the output above, @Autowired EntityManager received the same shared proxy. The reason is that the FactoryBean from section 1 implements SmartFactoryBean's getObject(Class), and when asked for the EntityManager type it hands out the shared proxy it built for itself (lines 667–676; the proxy is built at line 425). The container calls that method from FactoryBeanRegistrySupport (lines 202–203). The Javadoc of SmartFactoryBean.getObject(Class) says @since 7.0 (line 63). What @Autowired EntityManager does in earlier versions is not something this post checked in the source.

The two annotations are still not identical. @PersistenceContext has unitName (to choose between several factories), type = EXTENDED (an EntityManager that outlives a transaction) and properties, and getResourceToInject looks at them. EXTENDED takes a different path (ExtendedEntityManagerCreator, lines 810–822) that this post does not cover.

3. JpaTransactionManager — filling in the frame from Part 17

JpaTransactionManager extends AbstractPlatformTransactionManager from Part 17 and fills in doGetTransaction, doBegin, doCommit and doRollback (lines 117–118). The parent still decides, from the propagation setting, whether to open a new transaction or join one, so the only thing left to read is "what does opening mean for JPA?".

doGetTransaction() looks into the thread store with two keys — the factory (EntityManagerFactory) for an EntityManagerHolder, and the DataSource for a ConnectionHolder (lines 357–378). You do not have to configure the DataSource; afterPropertiesSet() asks the EntityManagerFactoryInfo from section 1 for it (lines 334–346). Why two keys is answered in doBegin.

	@Override
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		JpaTransactionObject txObject = (JpaTransactionObject) transaction;

		if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
			throw new IllegalTransactionStateException(
					"Pre-bound JDBC Connection found! JpaTransactionManager does not support " +
					"running within DataSourceTransactionManager if told to manage the DataSource itself. " +
					"It is recommended to use a single JpaTransactionManager for all transactions " +
					"on a single DataSource, no matter whether JPA or JDBC access.");
		}

		try {
			if (!txObject.hasEntityManagerHolder() ||
					txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
				EntityManager newEm = createEntityManagerForTransaction();
				// ...
				txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
			}

			EntityManager em = txObject.getEntityManagerHolder().getEntityManager();

			// Delegate to JpaDialect for actual transaction begin.
			int timeoutToUse = determineTimeout(definition);
			Object transactionData = getJpaDialect().beginTransaction(em,
					new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
			// ...
			// Register the JPA EntityManager's JDBC Connection for the DataSource, if set.
			if (getDataSource() != null) {
				ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
				if (conHandle != null) {
					ConnectionHolder conHolder = new ConnectionHolder(conHandle);
					// ...
					TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
					txObject.setConnectionHolder(conHolder);
				}
				// ...
			}

			// Bind the entity manager holder to the thread.
			if (txObject.isNewEntityManagerHolder()) {
				TransactionSynchronizationManager.bindResource(
						obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
			}
			txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
		}
		// ...
	}

spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java, lines 385–459

JpaTransactionManager.doBegin opens a fresh EntityManager for the transaction, starts the JPA transaction through JpaDialect.beginTransaction, then takes the JDBC connection held by the Hibernate session through JpaDialect.getJdbcConnection and binds a ConnectionHolder under the dataSource key and an EntityManagerHolder under the entityManagerFactory key. JdbcTemplate looks up the dataSource key, the shared EntityManager proxy looks up the entityManagerFactory key, and both reach the same JDBC connection. One transaction binds two things to the thread. The two yellow boxes are the entries in the store; the two grey boxes below are the parties that look them up, each with its own key.

  1. A fresh EntityManager is opened for this transaction (line 400, lines 470–484). This is the real object the shared proxy of section 2 will find.
  2. JpaDialect.beginTransaction starts the JPA transaction (line 411). The Hibernate dialect applies the isolation level or read-only flag to the connection if one was requested, then makes the standard call entityManager.getTransaction().begin() (HibernateJpaDialect line 147).
  3. The JDBC connection Hibernate is holding is taken out and bound under the DataSource key (lines 423–433). This is the reason for the second key. HibernateJpaDialect.getJdbcConnection unwraps the session and builds a handle that returns the physical connection inside it.
	private static class HibernateConnectionHandle implements ConnectionHandle {

		private final SessionImplementor session;

		public HibernateConnectionHandle(SessionImplementor session) {
			this.session = session;
		}

		@Override
		public Connection getConnection() {
			return this.session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection();
		}
	}

spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java, lines 263–275

  1. The EntityManagerHolder is bound under the factory key (lines 444–446).

Now when Part 18's JdbcTemplate calls DataSourceUtils.getConnection(dataSource) on the same thread, it looks in the store under the DataSource key (DataSourceUtils lines 103–106) and receives the very connection Hibernate is using. That is why JPA and SQL can be mixed in one transaction and still commit or roll back together. The class Javadoc makes the same point (lines 80–85).

doCommit calls EntityTransaction.commit() and translates any exception through the JpaDialect before rethrowing (lines 544–567). doRollback calls rollback() (lines 570–596), and afterwards doCleanupAfterCompletion unbinds both keys and closes the EntityManager (lines 609–646).

Example: does JdbcTemplate use the same connection?

The core of ex/u17/JpaJdbcSameConnectionExample.java.

// An example written for this post (not Spring's code)
tx.executeWithoutResult(status -> {
    // ...
    System.out.println("EntityManagerHolder bound? " + TransactionSynchronizationManager.hasResource(emf));
    System.out.println("ConnectionHolder bound?    " + TransactionSynchronizationManager.hasResource(ds));

    Connection fromJdbc = DataSourceUtils.getConnection(ds);                       // the path JdbcTemplate takes
    Connection fromJpa = em.unwrap(Session.class).doReturningWork(con -> con);    // the Hibernate session's connection
    System.out.println("same JDBC Connection?      " + (fromJdbc == fromJpa));

    em.persist(new Member("via-jpa"));
    em.flush();                                                                   // send the INSERT now
    jdbc.update("insert into member(id, name) values (?, ?)", 100, "via-jdbc");
    System.out.println("rows JdbcTemplate sees     : "
            + jdbc.queryForObject("select count(*) from member", Integer.class));
    status.setRollbackOnly();
});
transactionManager : JpaTransactionManager
dataSource auto-detected from EMF? true
--- outside transaction: ConnectionHolder bound? false
--- inside transaction
EntityManagerHolder bound? true
ConnectionHolder bound?    true
same JDBC Connection?      true
rows JdbcTemplate sees     : 2
--- after rollback
rows                       : 0
ConnectionHolder bound?    false

Both keys are bound, the Connection obtained by the two routes is the same object, and the row inserted through JPA and the row inserted through SQL disappear together in a single rollback.

Easy to get wrong

4. Exception translation — what does @Repository do?

Part 18 showed JdbcTemplate turning SQLException into Spring's DataAccessException hierarchy. JPA code does not go through JdbcTemplate, so it needs a different mechanism, and that is @Repository. The annotation itself is just @Component with an extra meaning (Repository lines 57–61); the real work happens once you register a post-processor called PersistenceExceptionTranslationPostProcessor as a bean.

PersistenceExceptionTranslationPostProcessor wraps a @Repository bean in a proxy in postProcessAfterInitialization. The advisor's pointcut checks whether the class carries @Repository; the interceptor catches RuntimeException, rethrows it unchanged if it is declared in the method's throws clause, and otherwise hands it to a chain of every PersistenceExceptionTranslator in the container. LocalContainerEntityManagerFactoryBean is one of those translators and delegates through the JpaDialect to HibernateExceptionTranslator, which produces a DataAccessException. The path an exception takes out of a @Repository bean's method. The red box is the case where it is passed through untranslated.

  1. The post-processor wraps the bean in a proxy. It is not the auto-proxy creator of Part 11 but a smaller family (AbstractAdvisingBeanPostProcessor) that carries exactly one advisor. In the after-initialisation hook it builds a proxy with ProxyFactory whenever the advisor applies to the class (lines 88–133; the check is AopUtils.canApply, line 170). The advisor is created in setBeanFactory (PersistenceExceptionTranslationPostProcessor lines 82–90).
  2. Advisor = pointcut + interceptor. The pointcut is an AnnotationMatchingPointcut asking "does the class carry @Repository?", and the advice is PersistenceExceptionTranslationInterceptor (PersistenceExceptionTranslationAdvisor lines 68–73). The annotation type can be changed, so a non-Spring annotation can get the same effect (lines 76–79).
  3. The interceptor catches the exception and translates it.
	@Override
	public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		catch (RuntimeException ex) {
			// Let it throw raw if the type of the exception is on the throws clause of the method.
			if (!this.alwaysTranslate && ReflectionUtils.declaresException(mi.getMethod(), ex.getClass())) {
				throw ex;
			}
			else {
				PersistenceExceptionTranslator translator = this.persistenceExceptionTranslator;
				if (translator == null) {
					// ...
						translator = detectPersistenceExceptionTranslators(this.beanFactory);
					// ...
					this.persistenceExceptionTranslator = translator;
				}
				throw DataAccessUtils.translateIfNecessary(ex, translator);
			}
		}
	}

spring-tx/src/main/java/org/springframework/dao/support/PersistenceExceptionTranslationInterceptor.java, lines 132–160

Unless you specify one, the translator is every PersistenceExceptionTranslator bean in the container, chained into one (lines 169–173). Look back at the declaration in section 1: AbstractEntityManagerFactoryBean implements exactly that interface (line 101). So configuring JPA gave you a translator for free, and that translator delegates to the JpaDialect (lines 579–582). The interface has a single method: return a DataAccessException if you can translate, null if you do not recognise the exception (PersistenceExceptionTranslator lines 35–56). On null, the original exception goes out as it is (DataAccessUtils lines 342–348).

  1. The Hibernate dialect translates in two steps. A HibernateException goes through Hibernate's own table (constraint violation → DataIntegrityViolationException, and so on, HibernateExceptionTranslator lines 167–169); anything else goes to the standard JPA table (lines 103–112). This is the JPA table.
	public static @Nullable DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex) {
		// ...
		if (ex instanceof EntityNotFoundException entityNotFoundException) {
			return new JpaObjectRetrievalFailureException(entityNotFoundException);
		}
		if (ex instanceof NoResultException) {
			return new EmptyResultDataAccessException(ex.getMessage(), 1, ex);
		}
		if (ex instanceof NonUniqueResultException) {
			return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex);
		}
		// ...
		if (ex instanceof TransactionRequiredException) {
			return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
		}

		// If we have another kind of PersistenceException, throw it.
		if (ex instanceof PersistenceException) {
			return new JpaSystemException(ex);
		}

		// If we get here, we have an exception that resulted from user code,
		// rather than the persistence provider, so we return null to indicate
		// that translation should not occur.
		return null;
	}

spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java, lines 477–525

Example: the same code, different annotation

ex/u17/RepositoryExceptionTranslationExample.java registers two classes with identical methods, one with @Repository and one with @Component.

// An example written for this post (not Spring's code)
@Repository
static class MemberRepository {
    @PersistenceContext
    EntityManager em;

    Member findByName(String name) {
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getSingleResult();               // jakarta.persistence.NoResultException if there is no row
    }

    void save(Member member) {
        em.persist(member);
    }
}
@Repository bean class : RepositoryExceptionTranslationExample$MemberRepository$$SpringCGLIB$$0
@Component  bean class : PlainDao
--- getSingleResult() with no row
@Component  : jakarta.persistence.NoResultException
@Repository : org.springframework.dao.EmptyResultDataAccessException (cause: NoResultException)
--- persist() outside a transaction
@Component  : jakarta.persistence.TransactionRequiredException
@Repository : org.springframework.dao.InvalidDataAccessApiUsageException (cause: TransactionRequiredException)
--- duplicate name, failure surfaces at commit
[main] WARN org.hibernate.orm.jdbc.error - HHH000247: ErrorCode: 23505, SQLState: 23505
[main] WARN org.hibernate.orm.jdbc.error - Unique index or primary key violation: "PUBLIC.UK9ESVGIKRMTI1V7CI6V453IMDC_INDEX_8 ON PUBLIC.MEMBER(NAME NULLS FIRST) VALUES ( /* 1 */ 'dup' )"; SQL statement:
insert into member (name,id) values (?,?) [23505-232]
@Repository : org.springframework.dao.DataIntegrityViolationException (cause: ConstraintViolationException)

Only the @Repository bean's class name ends in $$SpringCGLIB$$, marking it as a proxy, and the same calls produce Spring exceptions instead of JPA ones. The two WARN lines in the middle are Hibernate's logging.

Easy to get wrong

5. Reactive transactions — a world without threads

From here on we are in spring-r2dbc and the transaction.reactive package of spring-tx. Everything up to Part 17 leaned on "the current thread": the transaction manager binds resources to the thread, and JdbcTemplate and the shared proxy look them up there. Reactive code breaks that assumption. One piece of work moves across several threads, and one thread serves many pieces of work in turn. Whatever you put into a ThreadLocal may be gone, or belong to someone else, by the next step.

The interfaces come in pairs

Blocking (Part 17) Reactive (this post)
PlatformTransactionManager (line 47) ReactiveTransactionManager (line 36) — both extend the marker TransactionManager (line 28)
getTransaction → TransactionStatus getReactiveTransaction → Mono<ReactiveTransaction> (line 63)
AbstractPlatformTransactionManager AbstractReactiveTransactionManager (lines 84–85) — same propagation rules; doBegin and friends return Mono<Void> (line 817)
DataSourceTransactionManager R2dbcTransactionManager (line 84)
TransactionTemplate TransactionalOperator (line 53)
transaction.support.TransactionSynchronizationManager transaction.reactive.TransactionSynchronizationManager

The last row is the heart of this section. There are two classes with the same name.

public abstract class TransactionSynchronizationManager {

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java, lines 75–78

public class TransactionSynchronizationManager {

	private final TransactionContext transactionContext;
	// ...
	public static Mono<TransactionSynchronizationManager> forCurrentTransaction() {
		return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new);
	}
	// ...
	public @Nullable Object getResource(Object key) {
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		return doGetResource(actualKey);
	}

	/**
	 * Actually check the value of the resource that is bound for the given key.
	 */
	private @Nullable Object doGetResource(Object actualKey) {
		return this.transactionContext.getResources().get(actualKey);
	}

spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionSynchronizationManager.java, lines 70–120

The blocking one is all static methods over a ThreadLocal. The reactive one is an instance, a thin shell around a single TransactionContext object, and the resource map lives inside that object (TransactionContext lines 37–41). So where does the TransactionContext live?

Riding in the Reactor Context — TransactionContextManager

	public static Mono<TransactionContext> currentContext() {
		return Mono.deferContextual(ctx -> {
			if (ctx.hasKey(TransactionContext.class)) {
				return Mono.just(ctx.get(TransactionContext.class));
			}
			if (ctx.hasKey(TransactionContextHolder.class)) {
				TransactionContextHolder holder = ctx.get(TransactionContextHolder.class);
				if (holder.hasContext()) {
					return Mono.just(holder.currentContext());
				}
			}
			return Mono.error(new NoTransactionInContextException());
		});
	}

spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContextManager.java, lines 52–65

Mono.deferContextual is the Reactor operator for "decide at subscription time, looking at that subscription's Context". The TransactionContext sits in the Reactor Context under the key TransactionContext.class. It is attached to the subscription, not to a thread, so it follows the work even when the work changes threads. If it is missing, a lightweight variant of NoTransactionException is emitted (lines 117–121).

The ones that put it there are getOrCreateContext() and getOrCreateContextHolder() (lines 85–92, lines 103–109). The Javadoc explains why there is a separate holder: creating and releasing transactions proceeds top-down, while Reactor's Context is an immutable map propagated bottom-up (from the subscriber), so a mutable holder (with a stack inside, TransactionContextHolder lines 32–34) is placed in the Context and changed in place (lines 96–101).

The place that actually attaches both is the implementation of TransactionalOperator.

	@Override
	public <T> Flux<T> execute(TransactionCallback<T> action) throws TransactionException {
		return TransactionContextManager.currentContext().flatMapMany(context ->
			Flux.usingWhen(
				this.transactionManager.getReactiveTransaction(this.transactionDefinition),
				action::doInTransaction,
				this.transactionManager::commit,
				this::rollbackOnException,
				this.transactionManager::rollback)
			.onErrorMap(this::unwrapIfResourceCleanupFailure))
		.contextWrite(TransactionContextManager.getOrCreateContext())
		.contextWrite(TransactionContextManager.getOrCreateContextHolder());
	}

spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionalOperatorImpl.java, lines 74–86

TransactionalOperatorImpl.execute writes a TransactionContext into the Reactor Context with contextWrite. R2dbcTransactionManager.getReactiveTransaction reads it back through forCurrentTransaction and TransactionContextManager.currentContext and binds a ConnectionHolder in doBegin. DatabaseClient finds the same ConnectionHolder in that Context through ConnectionFactoryUtils.doGetConnection. The ThreadLocal TransactionSynchronizationManager stays empty. Where "the current transaction" lives in a reactive transaction. The yellow box is the TransactionContext inside the Reactor Context, and everything meets there. The red box is what nobody looks at.

Flux.usingWhen(resource, use, on success, on error, on cancel) is Reactor's "open a resource, use it, close it" operator. What TransactionTemplate.execute did with try/catch in Part 17 (open → run → commit, or roll back on an exception) is done by this one operator. And at the end of the chain sit the two contextWrite lines. In Reactor, contextWrite has to be placed downstream (towards the subscriber) for the operators upstream to see it, which is why it comes last. The transaction manager's getReactiveTransaction starts by pulling that Context out with forCurrentTransaction() (AbstractReactiveTransactionManager lines 116–120).

How @Transactional takes the reactive route

The TransactionInterceptor of Part 17 is unchanged. Its parent, TransactionAspectSupport.invokeWithinTransaction, has one branch near the top.

		final TransactionManager tm = determineTransactionManager(txAttr, targetClass);

		if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
			// ...
			ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
				// ...
				ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
				if (adapter == null) {
					throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +
							method.getReturnType() + "] with specified transaction manager: " + tm);
				}
				return new ReactiveTransactionSupport(adapter);
			});

			return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
		}

		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java, lines 339–360

If the chosen transaction manager is a ReactiveTransactionManager (and Reactive Streams is on the classpath, line 109, lines 192–197), the ReactiveAdapterRegistry is asked whether the method's return type is a reactive type such as Mono or Flux, and if not, an exception is thrown. If it is, the call is handed to the inner class ReactiveTransactionSupport (line 908), whose shape is the same as TransactionalOperatorImpl above: Mono.usingWhen to open, run and commit or roll back, with the two contextWrite lines at the end (lines 921–941).

R2dbcTransactionManager.doBegin

The reactive manager's doBegin receives the TransactionSynchronizationManager as a parameter (lines 183–184). The blocking one could call static methods from anywhere; here "the one for this subscription" has to be passed in. The body opens a connection (connectionFactory.create(), line 197), calls con.beginTransaction(...) (line 242), and on success binds a ConnectionHolder under the ConnectionFactory key.

						// Bind the connection holder to the thread.
						if (txObject.isNewConnectionHolder()) {
							synchronizationManager.bindResource(obtainConnectionFactory(), txObject.getConnectionHolder());
						}

spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java, lines 218–221

The comment says "to the thread", but the entry actually goes into the map inside the Context. The line has the same shape as its counterpart in Part 18's DataSourceTransactionManager, and the comment appears to have come along with it. doCommit and doRollback return a Mono and convert R2DBC exceptions to DataAccessException (lines 297–317).

Easy to get wrong: why ThreadLocal must not be used in reactive code

As we have seen, the reactive transaction manager never touches the ThreadLocal TransactionSynchronizationManager. So inside a reactive transaction, the blocking side's isActualTransactionActive() returns false (the example output below shows it). For the same reason, calling JdbcTemplate or the shared EntityManager proxy from inside a reactive chain makes them see an empty ThreadLocal and behave as if there were no transaction. The two worlds' mechanisms do not mix.

6. DatabaseClient and ConnectionFactoryUtils — the counterpart of DataSourceUtils

Finally, the "template" on the R2DBC side. DatabaseClient is a fluent API closer to JdbcClient than to JdbcTemplate, created with DatabaseClient.create(connectionFactory) (line 63, lines 109–111). The implementation, DefaultDatabaseClient, obtains its connection in a single line (lines 163–165), and that line is ConnectionFactoryUtils.getConnection, the counterpart of Part 18's DataSourceUtils.getConnection.

	public static Mono<Connection> doGetConnection(ConnectionFactory connectionFactory) {
		Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
		return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {

			ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory);
			if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
				conHolder.requested();
				if (!conHolder.hasConnection()) {
					return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection);
				}
				return Mono.just(conHolder.getConnection());
			}
			// Else we either got no holder or an empty thread-bound holder here.

			Mono<Connection> con = fetchConnection(connectionFactory);
			// ...
			return con;
		}).onErrorResume(NoTransactionException.class, ex -> Mono.from(connectionFactory.create()));
	}

spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java, lines 114–152

Put it next to Part 18's DataSourceUtils.doGetConnection (lines 103–106) and the structure is the same: look in the store under the ConnectionFactory key, return the holder's connection if there is one, otherwise open a new one. Three things differ. The store is taken from the Context with forCurrentTransaction(); the result is a Mono<Connection>; and the last line — when there is no transaction Context at all (NoTransactionException), simply open a new connection. That line is why DatabaseClient can be used outside a transaction. Releasing is symmetric: doReleaseConnection leaves a bound connection open and closes any other (lines 187–196). DefaultDatabaseClient adds Mono.usingWhen and a connection proxy that suppresses close() on top (lines 116–135, lines 191–195).

Role JDBC (Part 18) R2DBC
Resource factory (the store's key) DataSource ConnectionFactory
Store entry jdbc.datasource.ConnectionHolder (line 43) r2dbc.connection.ConnectionHolder (line 42)
Connection lookup DataSourceUtils.getConnection ConnectionFactoryUtils.getConnection
The store ThreadLocal TransactionContext inside the Reactor Context
Template JdbcTemplate, JdbcClient DatabaseClient

ConnectionFactoryUtils.doGetConnection(cf) (lines 114–152) looks for a TransactionContext in the Reactor Context with TransactionSynchronizationManager.forCurrentTransaction(). If there is no transaction in the Context, a NoTransactionException occurs and connectionFactory.create() opens a fresh connection each time, outside any transaction. If there is one, it looks for a ConnectionHolder under the key cf. If the holder exists and has a connection, that connection is returned — the path behind Example 3's 'same connection twice' being true. If the holder is bound to the transaction but has no connection yet, it opens one with fetchConnection and fills the holder. If there is no holder (or an unused empty one), it opens a new connection with fetchConnection and, if synchronization is active, binds a new ConnectionHolder with bindResource(cf, holder) (or fills the empty holder if there was one), so later calls take the same-connection path The same shape as DataSourceUtils.getConnection in Part 18, except the storage lives in the Reactor Context instead of a ThreadLocal. "Same connection → true" is the real output of step 3 in the example below.

Example: rollback and the Context

This is ex/u17/R2dbcTransactionExample.java. It connects to an in-memory H2 through the r2dbc-h2 driver.

// An example written for this post (not Spring's code)
R2dbcTransactionManager tm = new R2dbcTransactionManager(cf);
TransactionalOperator txOp = TransactionalOperator.create(tm);

// a piece of work whose second INSERT fails on the primary key
Mono<Void> work = client.sql("insert into member values (1, 'kim')").then()
        .then(client.sql("insert into member values (1, 'dup')").then());

// ... the three calls below are shown without their try/catch and printing
txOp.transactional(work).block();     // 1. inside a transaction → the first INSERT is rolled back too
work.block();                         // 2. without a transaction → the first INSERT stays

Mono<String> probe = Mono.deferContextual(ctx ->
                Mono.just("Reactor Context has TransactionContext? " + ctx.hasKey(TransactionContext.class)))
        .doOnNext(System.out::println)
        .then(TransactionSynchronizationManager.forCurrentTransaction()        // the transaction.reactive one
                .map(sm -> "reactive TSM: actualTransactionActive=" + sm.isActualTransactionActive()
                        + ", ConnectionHolder bound=" + sm.hasResource(cf)))
        .doOnNext(System.out::println)
        .then(Mono.fromCallable(() -> "ThreadLocal TSM (transaction.support): actualTransactionActive="
                + org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()
                + " on thread " + Thread.currentThread().getName()))
        .doOnNext(System.out::println)
        .then(Mono.zip(ConnectionFactoryUtils.getConnection(cf), ConnectionFactoryUtils.getConnection(cf))
                .map(t -> "same Connection twice inside the transaction? " + (t.getT1() == t.getT2())))
        .doOnNext(System.out::println);
txOp.transactional(probe).block();    // 3. where does the transaction live?
--- 1. inside a transaction: the first insert is rolled back too
[main] WARN io.r2dbc.h2.H2Connection - Option{name='readOnly', sensitive=false} + isn't supported in H2 at the transaction level. You must set it on conenction URL. See http://www.h2database.com/html/features.html#read_only
error : DuplicateKeyException
rows  : 0
--- 2. without a transaction: the first insert stays
error : DuplicateKeyException
rows  : 1
--- 3. where does the transaction live?
[main] WARN io.r2dbc.h2.H2Connection - Option{name='readOnly', sensitive=false} + isn't supported in H2 at the transaction level. You must set it on conenction URL. See http://www.h2database.com/html/features.html#read_only
Reactor Context has TransactionContext? true
reactive TSM: actualTransactionActive=true, ConnectionHolder bound=true
ThreadLocal TSM (transaction.support): actualTransactionActive=false on thread main
same Connection twice inside the transaction? true
--- 4. outside: no TransactionContext in the Reactor Context
NoTransactionInContextException : No transaction in context

The two WARN lines come from the H2 driver and have nothing to do with this post. Comparing 1 and 2, only the work wrapped in TransactionalOperator had its first INSERT undone. Part 3 is the point of this section: although everything ran on the same main thread, the Reactor Context holds a transaction while the ThreadLocal holds none, and calling ConnectionFactoryUtils.getConnection twice yields the same connection. As in part 4, calling currentContext() outside a transaction gives "No transaction in context".

If you want to go deeper

Only the names and locations of what was left out. The EXTENDED persistence context that outlives a transaction, and the ExtendedEntityManagerCreator that builds it (PersistenceAnnotationBeanPostProcessor lines 810–822). The bootstrapExecutor that builds the factory on a background thread (lines 411–417). LocalSessionFactoryBean and HibernateTransactionManager in the orm.jpa.hibernate package, for using Hibernate's own API without JPA. The SharedEntityAgentInvocationHandler, which exposes a JPA 4.0 EntityAgent as a shared proxy (its Javadoc says @since 7.0.4; around SharedEntityManagerCreator line 425). On the reactive side, the branch that treats Kotlin coroutines (suspend functions) as Mono/Flux (TransactionAspectSupport lines 342–349), R2DBC savepoints (R2dbcTransactionManager lines 188–191), NamedParameterExpander (the r2dbc.core package) and BindMarkersFactory (the r2dbc.core.binding package), which turn DatabaseClient's named parameters (:name) into the driver's ? or $1, and TransactionalEventPublisher for reactive transactional events.

Recap

Concept In one line
LocalContainerEntityManagerFactoryBean A FactoryBean. In afterPropertiesSet it settles the persistence unit, builds the factory through the provider, wraps it in a proxy and exposes that
packagesToScan Without persistence.xml, DefaultPersistenceUnitManager builds a default unit and scans for @Entity
JpaVendorAdapter Confines vendor differences to three things: provider, properties, JpaDialect
The EntityManagerFactory bean A JDK proxy. Through EntityManagerFactoryInfo it reveals the DataSource, the dialect and the native factory
@PersistenceContext PersistenceAnnotationBeanPostProcessor injects a shared proxy. Since 7.0, @Autowired EntityManager gets the same proxy
The shared EntityManager proxy On every call, looks in the thread store for the current transaction's EntityManager. Without one, writes throw and reads use a temporary object
JpaTransactionManager.doBegin Opens an EntityManager, begins the JPA transaction, then binds two holders — under the factory key and under the DataSource key
JPA and JdbcTemplate JdbcTemplate receives Hibernate's connection through the DataSource key, so commit and rollback happen together in one transaction
@Repository Only a marker. PersistenceExceptionTranslationPostProcessor adds the proxy and the interceptor translates through the PersistenceExceptionTranslator beans
ReactiveTransactionManager The counterpart of PlatformTransactionManager. Same propagation rules, results as Mono
transaction.reactive.TransactionSynchronizationManager A different class with the same name. Looks at the TransactionContext in the Reactor Context instead of a ThreadLocal
TransactionalOperator / reactive @Transactional Open, run and close with usingWhen, and load the TransactionContext with contextWrite at the end
ConnectionFactoryUtils.getConnection The counterpart of DataSourceUtils.getConnection. Returns the connection bound in the Context, or opens a new one when there is no transaction

What to look at next

With this post, Phase 4 of the map in Part 1 (transactions and data access, U15–U17) is complete. The transaction abstraction of Part 17, JdbcTemplate in Part 18, and the JPA and R2DBC integration here all had the same shape: bind a resource in a store under a key, and let the users find it with that key. The only difference was whether the store is a thread or a Context.

Next comes Phase 5, the web. Before following a single web request all the way to its response, we look at the floor of the spring-web module: HttpMessageConverter, which turns a request body into an object, WebApplicationContext, which starts the Spring container inside a servlet container, and the filters that step in before and after a request.

More posts