spring-projects / spring-modulith

Modular applications with Spring Boot
https://spring.io/projects/spring-modulith
Apache License 2.0
710 stars 111 forks source link

Support Event Publication Registry with multiple TransactionManagers (separate PostgreSQL schemas) #683

Open znaczek opened 1 week ago

znaczek commented 1 week ago

I have an application with two @ApplicationModule: orders and products. I want to have a separate PostgreSQL schema for each of them using spring-boot-starter-data-jpa. That's how I achieved the desired configuration. TransactionManagers setup:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.modularmonolithexample.products.domain"}, entityManagerFactoryRef =
        "productsEntityManagerFactory", transactionManagerRef = "productsTransactionManager")
@EntityScan("com.modularmonolithexample.products.domain.*")
public class ProductsPersistenceConfiguration {

    @Bean
    @ConfigurationProperties("spring.datasource.products")
    public DataSourceProperties productsDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource productsDataSource() {
        return productsDataSourceProperties().initializeDataSourceBuilder().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean productsEntityManagerFactory(@Qualifier("productsDataSource") DataSource dataSource,
                                                                               EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dataSource).packages("com.modularmonolithexample.products.domain").build();
    }

    @Bean
    @Qualifier("productsTransactionManager")
    public PlatformTransactionManager productsTransactionManager(
            @Qualifier("productsEntityManagerFactory") LocalContainerEntityManagerFactoryBean productsEntityManagerFactory) {
        return new JpaTransactionManager(Objects.requireNonNull(productsEntityManagerFactory.getObject()));
    }

}

and

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.modularmonolithexample.orders.domain"}, entityManagerFactoryRef =
        "ordersEntityManagerFactory", transactionManagerRef = "ordersTransactionManager")
@EntityScan("com.modularmonolithexample.orders.domain")
public class OrdersPersistenceConfiguration {

    @Bean
    @ConfigurationProperties("spring.datasource.orders")
    public DataSourceProperties ordersDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    public DataSource ordersDataSource() {
        return ordersDataSourceProperties().initializeDataSourceBuilder().build();
    }

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean ordersEntityManagerFactory(@Qualifier("ordersDataSource") DataSource dataSource,
                                                                             EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dataSource).packages("com.modularmonolithexample.orders.domain").build();
    }

    @Bean
    @Qualifier("ordersTransactionManager")
    public PlatformTransactionManager ordersTransactionManager(
            @Qualifier("ordersEntityManagerFactory") LocalContainerEntityManagerFactoryBean ordersEntityManagerFactory) {
        return new JpaTransactionManager(Objects.requireNonNull(ordersEntityManagerFactory.getObject()));
    }

}

I configure them with the following properties file:

spring:
  datasource:
    orders:
      url: jdbc:postgresql://localhost:5432/modular-monolith-example?currentSchema=orders
      username: postgres
      password: admin
    products:
      url: jdbc:postgresql://localhost:5432/modular-monolith-example?currentSchema=products
      username: postgres
      password: admin

With the above setup application (without Event Publication Registry configured yet) works correctly.

I added spring-modulith-starter-jpa, made use of @TransactionalEventListere and created the event_publication table according to the documentation in both schemas - the goal is to have separate event_publication table for every module db schema.

When I tested it, I was getting the following error when dispatching an event using ApplicationEventPublisher:

No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: ordersTransactionManager,productsTransactionManager

As I understand, this is cause by #JpaEventPublicationConfiguration#jpaEventPublicationRepository accepts EntityManager em without any @Qualifier and my setup doesn't provide one.

So I tried to add @Primary to one of the qualified TransacitonManagers (this is against the goal above, but at least I want to try it that helps), but then I was getting another error:

Unable to locate persister: org.springframework.modulith.events.jpa.JpaEventPublication

which looks similar to #345. However, when I try to follow the suggestion and add org.springframework.modulith.events.jpa to @EntityScan like this:

@EntityScan({"com.modularmonolithexample.products.domain.*", "org.springframework.modulith.events.jpa"})

It doesn't help in my case.

A also tried using spring-modulith-starter-jdbc insted instead. At the beginning it was throwing the same No qualifying bean exception. I tried again to add @Primary to one of the TransactionManagers. But the the result was quite strange - events were stored in orders.event_publication regardless of on which TransactionManager bean I was putting @Primary (🤯).


At this point I am running out of ideas... Thanks for any help!


EDIT After a while I tried with the default DataSource and TransactionManager (autoconfigured by Spring) and I defined the schema in @Table annotation. That worked, however with this I had to define single event_publication in public schema. Still have no workaround for working with separate TransactionManagers

odrotbohm commented 6 days ago

the goal is to have separate event_publication table for every module db schema.

That has been a non-goal so far. I wonder how / whether this would work reasonably well in practice. Primarily because the listener decoration would need to know which schema to use to mark the publication completed. This logically ties the two transactions anyway (the publishing one and the consuming one). What is supposed to happen if the publishing transaction is running against the DataSource for schema one, but a listener is declared to run on schema two?

Is it an option to rather use one DataSource and assign the individual entities to dedicated schemas via @Table(schema = …)?

odrotbohm commented 6 days ago

I see the @Table annotation works for you. You should be able to override the schema for the EventPublication JPA entity in orm.xml. In JPA we have little influence on any mapping customizations that you might want to apply. I wonder if it makes sense to add a property to the JDBC implementation that'd allow defining a custom schema to be used for that table.