Add multi-datasource module: two databases with per-database DataSource, Flyway, EntityManagerFactory and transaction manager on Boot 4.1

Working setup plus one small application per way of getting it wrong (no @Primary, @Primary only, url vs jdbc-url, wrong repository package, Flyway auto-configuration), plain vs named @Transactional, and ChainedTransactionManager with a failing commit. Transcripts are written by the tests.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Uu7q8vPeREyT4218EJPzz1
This commit is contained in:
Claude
2026-09-24 09:38:27 +00:00
parent d6b76c57db
commit e59ff029a1
44 changed files with 1465 additions and 0 deletions
@@ -0,0 +1,25 @@
# Two databases, each with its own DataSource, Flyway, EntityManagerFactory and transaction manager
--- the two DataSources (one pool each, configured separately) ---
customersDataSource pool=customers-pool max=4 url=jdbc:h2:mem:customers primary=true
ordersDataSource pool=orders-pool max=8 url=jdbc:h2:mem:orders primary=false
--- Flyway ran once per database, each with its own history ---
customers flyway_schema_history: [1 customer]
orders flyway_schema_history: [1 purchase order, 2 index customer]
--- what each EntityManagerFactory manages ---
EntityManagerFactory beans: [customersEntityManagerFactory, ordersEntityManagerFactory]
customersEntityManagerFactory entities=[Customer]
ordersEntityManagerFactory entities=[PurchaseOrder]
--- transaction managers ---
beans: [customersTransactionManager, ordersTransactionManager]
customersTransactionManager primary=true
ordersTransactionManager primary=false
--- save one customer and one order through the two repositories ---
customers database tables: [CUSTOMER, flyway_schema_history]
orders database tables: [PURCHASE_ORDER, flyway_schema_history]
rows: customers.customer=1 orders.purchase_order=1
@@ -0,0 +1,4 @@
# Two DataSource beans, none marked @Primary
startup failed: BeanCreationException -> NoSuchBeanDefinitionException
root cause: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' available
@@ -0,0 +1,8 @@
# One @Primary DataSource and nothing else: it starts, and it writes to the wrong database
application started: yes
EntityManagerFactory beans: [entityManagerFactory]
customers database tables: [CUSTOMER, PURCHASE_ORDER]
orders database tables: []
rows in the customers database: customer=1 purchase_order=1
hibernate.hbm2ddl.auto on that factory: create-drop
@@ -0,0 +1,4 @@
# DataSourceBuilder.create().build() bound to app.datasource.customers.url
startup failed: BeanCreationException -> BeanInstantiationException -> IllegalArgumentException
root cause: java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.
@@ -0,0 +1,6 @@
# A hand-built EntityManagerFactory that sets no Hibernate properties of its own
hibernate.physical_naming_strategy = org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
hibernate.implicit_naming_strategy = org.springframework.boot.hibernate.SpringImplicitNamingStrategy
hibernate.hbm2ddl.auto = null
saved a Customer whose field is createdAt into a column created_at: id=1
@@ -0,0 +1,25 @@
# Spring Boot's Flyway auto-configuration when there are two databases
--- A. nothing configured: Flyway targets the @Primary DataSource and scans classpath:db/migration ---
startup failed: BeanCreationException -> BeanCreationException -> FlywayException
root cause: org.flywaydb.core.api.FlywayException: Found more than one migration with version 1
Offenders:
-> <multi-datasource>/target/classes/db/migration/customers/V1__customer.sql (SQL)
-> <multi-datasource>/target/classes/db/migration/orders/V1__purchase_order.sql (SQL)
--- B. spring.flyway.locations=classpath:db/migration/customers ---
startup failed: BeanCreationException -> PersistenceException -> SchemaManagementException
root cause: org.hibernate.tool.schema.spi.SchemaManagementException: Schema validation: missing table [purchase_order]
application started (Hibernate validation switched off so the databases can be inspected)
customers database tables: [CUSTOMER, flyway_schema_history]
orders database tables: []
--- C. as B, plus one hand-made Flyway bean for the orders database ---
startup failed: BeanCreationException -> PersistenceException -> SchemaManagementException
root cause: org.hibernate.tool.schema.spi.SchemaManagementException: Schema validation: missing table [customer]
application started (Hibernate validation switched off so the databases can be inspected)
customers database tables: []
orders database tables: [PURCHASE_ORDER, flyway_schema_history]
@@ -0,0 +1,4 @@
# A repository for Customer scanned against the orders EntityManagerFactory
startup failed: BeanCreationException -> IllegalArgumentException
root cause: java.lang.IllegalArgumentException: Not a managed type: class com.ankurm.multids.customers.Customer
@@ -0,0 +1,10 @@
# A service method that writes to both databases, then throws
--- @Transactional (no name): Spring picks the @Primary manager, customersTransactionManager ---
thrown: java.lang.IllegalStateException: boom after both writes
rows after the rollback: customers.customer=0 orders.purchase_order=1
--- @Transactional("ordersTransactionManager"): the other database is now the one that rolls back ---
thrown: java.lang.IllegalStateException: boom after both writes
rows after the rollback: customers.customer=1 orders.purchase_order=0
@@ -0,0 +1,13 @@
# ChainedTransactionManager over the customers and orders managers
--- 1. business exception after both writes: both roll back ---
rows: customers.customer=0 orders.purchase_order=0
--- 2. the orders database refuses to commit; orders is LAST in the list ---
thrown: org.springframework.transaction.HeuristicCompletionException (outcome: rolled back)
rows: customers.customer=0 orders.purchase_order=0
--- 3. the orders database refuses to commit; orders is FIRST in the list ---
thrown: org.springframework.transaction.HeuristicCompletionException (outcome: mixed)
rows: customers.customer=1 orders.purchase_order=0
@@ -0,0 +1,11 @@
# Is org.springframework.data.transaction.ChainedTransactionManager still shipped, and is it deprecated?
jar: spring-data-commons-4.1.1.jar
class: org/springframework/data/transaction/ChainedTransactionManager.class
--- javap -v: class-level annotations ---
RuntimeVisibleAnnotations:
0: #245()
java.lang.Deprecated
--- javap -v: Deprecated attribute ---
Deprecated: true
@@ -0,0 +1,6 @@
# javac on a class carrying two @EnableJpaRepositories (scripts/facts/RepeatedEnable.java)
RepeatedEnable.java:7: error: EnableJpaRepositories is not a repeatable annotation interface
@EnableJpaRepositories(basePackages = "a.orders", entityManagerFactoryRef = "ordersEntityManagerFactory")
^
1 error
@@ -0,0 +1,9 @@
# What does EntityManagerFactoryBuilder.Builder.packages(Class...) do with the classes it is given?
jar: spring-boot-jpa-4.1.1.jar
--- javap -c: the calls made inside packages(java.lang.Class<?>...) ---
invokespecial #34 // Method java/util/HashSet."<init>":()V
invokestatic #35 // Method org/springframework/util/ClassUtils.getPackageName:(Ljava/lang/Class;)Ljava/lang/String;
invokeinterface #41, 2 // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
invokestatic #47 // Method org/springframework/util/StringUtils.toStringArray:(Ljava/util/Collection;)[Ljava/lang/String;