17

I have an application which is using Spring data JPA and hibernate envers for db auditing. Since R2DBC doesn't support auditing yet, is it possible to use combination of both in a single application?

If yes, Plan is to use Spring Data JPA for insert, update and delete operations, so that all DB auditing will be handled by hibernate envers. And use R2DBC for reactive non-blocking API's to read data.

If no, Is there any suggestions on how to achieve both reactive API's and auditing?

Purushotham
  • 205
  • 3
  • 10
  • 2
    Yes, this should work. – Jens Schauder Jun 08 '20 at 06:49
  • You could potentialy use R2DBC for all operations and use a library (if available) to connect to the database log (e.g. mysql-binlog-connector-java) in a separate process which will catch database changes and then you persist them in your audit tables. – NikolaB Jun 17 '20 at 11:43
  • 1
    Try Hibernate Reactive(a new project under Hibernate, currently lots of limits), not sure it supports the auditing feature now. – Hantsy Sep 07 '20 at 14:28

3 Answers3

9
  1. Spring provided simple Auditing via @EnableR2dbcAuditing, check my example.

  2. Mix JPA in a reactive application is also possible, I have an example to demo run JPA in a reactive application, but not added r2dbc in it.

  3. For your plan, a better solution is applying the cqrs pattern on the database topology, use database cluster for your application.

    • JPA for applying changes, use the main/master database to accept the modification, and sync changes to the secondary/slave database.
    • r2dbc for queries as you expected, use the secondary/slave database to query.
    • use a gateway at the front for the query and command service.

Update: I have created a sample to demo JPA and R2dbc coexistence in a single webflux application. But I do not suggest using it in real-world applications. Consider the 3rd solution above if you really need it.

Hantsy
  • 8,006
  • 7
  • 64
  • 109
  • Could you explain more about why it is better to use JPA and R2DBC separately each for Command and Query, not together? I'm just curious how can I make relationships between multiple tables and join them easily in R2DBC without any help of JPA. – Wood May 12 '22 at 18:34
  • For complex queries, use R2dbc `DatabaseClient` to [execute the raw sql and handling the result](https://github.com/hantsy/spring-r2dbc-sample/blob/master/database-client/src/main/java/com/example/demo/PostRepository.java#L36) yourself, or use JOOQ(support R2dbc since 3.15) to write [type-safe queries](https://github.com/hantsy/spring-r2dbc-sample/blob/master/jooq/src/main/java/com/example/demo/DemoApplication.java#L72). – Hantsy May 13 '22 at 01:54
  • Then, how about using DatabaseClient both on Command and Query? I think you can't get so many advantages of ORM if you use only to write data to DB. And how about QueryDSL instead of JOOQ? I think it is more popular and (AFAIK) supports r2dbc as an extension. – Wood May 14 '22 at 02:05
  • As I know QueryDSL is not active as JOOQ, and it does not support R2dbc, see: https://github.com/querydsl/querydsl/issues/2468 – Hantsy May 14 '22 at 03:17
1

Yes it is possible however you will probably face two issues, one is that multiple repos modules handling needs to be explicited (e.g specify the paths of the respective modules).

Secondly, the JDBC/JPA Spring Boot autoconfiguration will be disabled and you need to import it back. Those issues have been reported in Reactive and Non Reactive repository in a Spring Boot application with H2 database And a solution to them and the thought process can be found in this issue: https://github.com/spring-projects/spring-boot/issues/28025

LifeIsStrange
  • 11
  • 1
  • 2
0
**Here are the changes I did :
I am trying to catch a trigger from postgres on updation of the table content.** 

**R2dbc Config**

@Configuration
public class R2dbcConfig {

    @Value("${spring.r2dbc.url}")
    private String url;

    @Value("${spring.r2dbc.name}")
    private String name;

    @Value("${spring.r2dbc.username}")
    private String username;

    @Value("${spring.r2dbc.password}")
    private String password;
    @Bean
    public ConnectionFactory connectionFactory() {
        return new PostgresqlConnectionFactory(
                PostgresqlConnectionConfiguration.builder()
                        .host(url)
                        .database(name)
                        .username(username)
                        .password(password)
                        .build()
        );
    }

    @Bean
    DatabaseClient databaseClient(ConnectionFactory connectionFactory) {
        return DatabaseClient.builder()
                .connectionFactory(connectionFactory)
                .namedParameters(true)
                .build();
    }
}

**Jpa Config:**

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.artemis.repositories")
@EntityScan("com.artemis.entities")
@Slf4j
public class JpaConfig implements EnvironmentAware {

    private static final String ENV_HIBERNATE_DIALECT = "hibernate.dialect";
    private static final String ENV_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
    private static final String ENV_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
    private static final String ENV_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
    private Environment env;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(
                env.getProperty("datasource.url"),
                env.getProperty("datasource.username"),
                env.getProperty("datasource.password")
        );
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(dataSource);
        emf.setPackagesToScan(ArtemisApplication.class.getPackage().getName());
        emf.setPersistenceProvider(new HibernatePersistenceProvider());
        emf.setJpaProperties(jpaProperties());
        return emf;
    }

    private Properties jpaProperties() {
        Properties extraProperties = new Properties();
        extraProperties.put(ENV_HIBERNATE_FORMAT_SQL, env.getProperty(ENV_HIBERNATE_FORMAT_SQL));
        extraProperties.put(ENV_HIBERNATE_SHOW_SQL, env.getProperty(ENV_HIBERNATE_SHOW_SQL));
        extraProperties.put(ENV_HIBERNATE_HBM2DDL_AUTO, env.getProperty(ENV_HIBERNATE_HBM2DDL_AUTO));
        if (log.isDebugEnabled()) {
            log.debug(" hibernate.dialect @" + env.getProperty(ENV_HIBERNATE_DIALECT));
        }
        if (env.getProperty(ENV_HIBERNATE_DIALECT) != null) {
            extraProperties.put(ENV_HIBERNATE_DIALECT, env.getProperty(ENV_HIBERNATE_DIALECT));
        }
        return extraProperties;
    }

    @Bean
    public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory.getObject());
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.env = environment;
    }
}

And my service class:

class myService{

    final PostgresqlConnection connection;

    public myService(ConnectionFactory connectionFactory ) {
        this.connection =  Mono.from(connectionFactory.create())
                .cast(PostgresqlConnection.class).block();
    }

    @PostConstruct
    private void postConstruct() {
        connection.createStatement("LISTEN my_channel").execute()
                .flatMap(PostgresqlResult::getRowsUpdated).subscribe();
        connection.getNotifications().subscribe(myService::catchTrigger);
    }

    private static void catchTrigger(Notification notification) {
        System.out.println(notification.getName());
        System.out.println(notification.getParameter());
    }
}
Arsh
  • 1