0

Okay, I've read quite a lot of post around this and read spring boot documentation on this but never really got an answer which is cleaner approach.

Here my use case: We have like 100-200 oracle entities and we're using JPARepository interface to query them. Now we need to make sure read db is used for read calls and write db should be used for any writes.

We have a spring boot application, using HikariCP datasource and configured it using @EnableTransactionManagement, @EnableJpaRepositories passing reference of entity manager, transaction manager and base packages to scan.

I've created two configuration files, one with ReadConfiguration and one with WriteConfiguration. Now problem is, we have code in a standard OO way where we have service and repository layer. Different services are injecting various repositories. Each repository interface is extending JpaRepository and that interface is autowired in lot of service classes.

What I want to achieve is to use the same repository layer but somehow repository layer should know automatically that if it's a read call then use read datasource and if it's a write call use a write data source.

One of the solution is to use some kind of way to hack the proxy implementation of repository to have some logic to see if it's read call then use this read datasource if it's a write call then use write datasource. Has anyone solved this in a this way? Essentially what I need is to route call to datasource depending on the method call. If method is doing some read operation then I need to make sure it use read datasource or else use write datasource.

Can anyone point in some direction on how to achieve with this kind of architecture without writing new repository layer for read and write.

Thanks.

User5817351
  • 989
  • 2
  • 16
  • 36

3 Answers3

2

I think u can use the AbstractRoutingDataSource as mentioned in this repo. https://github.com/kwon37xi/replication-datasource

With this approach u may add @Transactional method with extra property to indicate if its read or write. @Transactional(readOnly = true|false)

Praneeth Ramesh
  • 3,434
  • 1
  • 28
  • 34
  • Looked at it at cursory, this way I would need to update like 100-200 service layer to add transaction annotation, which I'm trying to avoid. Rather what I would would do is add couple of new classes that can get the job done. Also we are using HikariCP datasource not sure if I can use above methods to fork Spring JDBC classes. – User5817351 Apr 13 '17 at 21:10
  • Also in some cases one service method is doing a read call and then write call. So in this case, I will need to use write datasource on such service methods. – User5817351 Apr 13 '17 at 21:13
1

I don't think this is actually possible (see below), but this an approach to at least get close.

I'd start with the code of LazyConnectionDataSource because due to your requirements you must not obtain a connection before you actually know what you are going to do with it.

Give it a reference to the session, so when it actually needs a Connection it inspects the session, to see if it is dirty and decide based on that which actual DataSource used.

Now to the reason why I don't think this will actually work. In a normal use case a transaction consists of:

  1. reading some data

  2. doing some changes based on it

  3. persisting these changes.

These steps happen strictly sequential and although we have types like Future in Java we can't actually look into the future and thus are unable at the first step to decide if there will be a third step.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • Thanks for your response. Then I guess I would need to register different entity manager for different repository i.e. that way I would have write datasource (write datasource can do read as well) defined for repositories that are doing any write operation and read datasource for repositories that are doing read datasource. That would be easier task I guess. – User5817351 Apr 14 '17 at 15:22
0

My guess is this could be seen as a multi-tenant approach, one tenant would be readonly, the other tenant would be write.

The steps to configure the persistence layer for multitenancy support includes:

  • Hibernate, JPA and datasources properties. Something like:

application.yml

...
multitenancy:
  dvdrental:
    dataSources:
      -
        tenantId: readonly
        url: jdbc:postgresql://172.16.69.133:5432/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
      -
        tenantId: write
        url: jdbc:postgresql://172.16.69.133:5532/db_dvdrental
        username: user_dvdrental
        password: changeit
        driverClassName: org.postgresql.Driver
...

MultiTenantJpaConfiguration.java

 ...
 @Configuration
 @EnableConfigurationProperties({ MultiTenantDvdRentalProperties.class, JpaProperties.class })
 @ImportResource(locations = { "classpath:applicationContent.xml" })
 @EnableTransactionManagement
 public class MultiTenantJpaConfiguration {

   @Autowired
   private JpaProperties jpaProperties;

   @Autowired
   private MultiTenantDvdRentalProperties multiTenantDvdRentalProperties;
 ...
 }

MultiTenantDvdRentalProperties.java

...
@Configuration
@ConfigurationProperties(prefix = "multitenancy.dvdrental")
public class MultiTenantDvdRentalProperties {

  private List<DataSourceProperties> dataSourcesProps;
  // Getters and Setters

  public static class DataSourceProperties extends org.springframework.boot.autoconfigure.jdbc.DataSourceProperties {

    private String tenantId;
    // Getters and Setters
  }
}
  • Datasources beans

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean(name = "dataSourcesDvdRental" )
   public Map<String, DataSource> dataSourcesDvdRental() {
       ...
   }
 ...
 }
  • Entity manager factory bean

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public MultiTenantConnectionProvider multiTenantConnectionProvider() {
       ...
   }

   @Bean
   public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
       ...
   }

   @Bean
   public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
     CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
       ...  
   }
 ...
 }
  • Transaction manager bean

MultiTenantJpaConfiguration.java

 ...
 public class MultiTenantJpaConfiguration {
 ...
   @Bean
   public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
       ...
   }

   @Bean
   public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
       ...
   }
 ...
 }
  • Spring Data JPA and transaction support configuration

applicationContent.xml

...
<jpa:repositories base-package="com.asimio.dvdrental.dao" transaction-manager-ref="txManager" />
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
...

ActorDao.java

public interface ActorDao extends JpaRepository<Actor, Integer> {
}

Depending on your needs something like this could be done:

...
@Autowired
private ActorDao actorDao;
...

// Read feature
...
DvdRentalTenantContext.setTenantId("readonly");
this.actorDao.findOne(...);
...

// Or write
DvdRentalTenantContext.setTenantId("write");
this.actorDao.save(...);
...

Setting the tenantId could be done in a servlet filter / Spring MVC interceptor / thread that is going to execute the JPA operation, etc.

More detail about multi-tenancy approach could be found at my blog at http://tech.asimio.net/2017/01/17/Multitenant-applications-using-Spring-Boot-JPA-Hibernate-and-Postgres.html

ootero
  • 3,235
  • 2
  • 16
  • 22