axon

AXON: Use MySql for EventStore and MongoDb for Projection - Events recreating records


I have an existing app that saved collections in MongoDb. As mentioned in documentation and on https://www.youtube.com/watch?v=342tqAORbAM that MongoDB is not right and that I should use plain RDBMS like MySQL /- Axon Server.

I am able to do that but when I restart the server, all the events are replayed and I get collections repeated again.

Here's how my code looks like.

Below, I am using MongoDb for saving Products in ProductDB and MySQL for eventStore.

Application Yaml

spring:
  data:
    mongodb:
      database: productsDB
      port: 27017
      host: localhost
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  datasource:
    url: jdbc:mysql://localhost:3306/mysql?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
    username: dinesh
    password: abc123
    name: mysql
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect

##Axon configuration
axon:
  serializer:
    events: jackson
    general: jackson
    messages: jackson

In Pom.Xml, I tried excluding axon-server-connector and including as well.

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
            <version>2.7.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${springfox.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox.version}</version>
        </dependency>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-spring-boot-starter</artifactId>
            <version>${axon-spring-boot-starter.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.axonframework</groupId>
                    <artifactId>axon-server-connector</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>${commons-beanutils.version}</version>
        </dependency>

        <dependency>
            <groupId>org.axonframework.extensions.mongo</groupId>
            <artifactId>axon-mongo</artifactId>
            <version>${axon-mongo.version}</version>
        </dependency>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-metrics</artifactId>
            <version>${axon-metrics.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.31</version>
        </dependency>

Here's my AxonConfig


@Configuration
public class AxonConfig {

    @Bean
    public EntityManagerProvider getEntityManagerProvider(){
        return new ContainerManagedEntityManagerProvider();
    }


   /* @Bean
    public EventStore eventStore(EventStorageEngine storageEngine, GlobalMetricRegistry metricRegistry) {

        return EmbeddedEventStore.builder()
                .storageEngine(storageEngine)
                .messageMonitor(metricRegistry
                        .registerEventBus("eventStore"))

                .build();
    }*/
   @Bean
   public TokenStore tokenStore( EntityManagerProvider entityManagerProvider) {
       return JpaTokenStore.builder().entityManagerProvider(entityManagerProvider)
               .serializer(JacksonSerializer.defaultSerializer()).build();
   }



    @Bean
    public EventStorageEngine eventStorageEngine(Serializer serializer,
                                                 PersistenceExceptionResolver persistenceExceptionResolver,
                                                 @Qualifier("eventSerializer") Serializer eventSerializer,
                                                 EntityManagerProvider entityManagerProvider,
                                                 TransactionManager transactionManager) throws SQLException {

        JpaEventStorageEngine eventStorageEngine = JpaEventStorageEngine.builder()
                .snapshotSerializer(serializer)
                .persistenceExceptionResolver(persistenceExceptionResolver)
                .eventSerializer(serializer)
                .entityManagerProvider(entityManagerProvider)
                .transactionManager(transactionManager)
                .build();

        return eventStorageEngine;
    }

    @Bean
    public EventUpcasterChain eventUpcasters(){
        return new EventUpcasterChain();
    }


}

I start my axon server and Application.

Step 1: Http POST Create a new product (WORKS) Step 2: On Axon Server dashboard , event is created (WORKS) Step 3: In MongoDb, Product collection is created and entry can be verified (WORKS)

Step 4: Stop the Springboot applicatio and restart. Step 5: MongoDb has an additional record because event was played again.

What's missing here? Here's what I already tried:

  1. Create EventStore also in MOngo (Works) but I don't want to use Mongo for eventStore and want to stick to MySql/PostgreSql for Axon server ( All examples on Youtube, documents just show with H2)

  2. Tried adding and also removing the axon-server-connector (DomainEventEntry is not created in either case)

  3. When I enable the code to exclude

 <exclusions>
                <exclusion>
                    <groupId>org.axonframework</groupId>
                    <artifactId>axon-server-connector</artifactId>
                </exclusion>
            </exclusions>

I get below error

2022-12-03 03:24:47.844  WARN 43476 --- [cessor[event]-0] o.a.e.TrackingEventProcessor             : Fetch Segments for Processor 'event' failed: org.hibernate.hql.internal.ast.QuerySyntaxException: DomainEventEntry is not mapped [SELECT MIN(e.globalIndex) - 1 FROM DomainEventEntry e]. Preparing for retry in 1s

java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: DomainEventEntry is not mapped [SELECT MIN(e.globalIndex) - 1 FROM DomainEventEntry e]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:757) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:848) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:114) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311) ~[spring-orm-5.3.24.jar:5.3.24]
    at com.sun.proxy.$Proxy147.createQuery(Unknown Source) ~[na:na]
    at org.axonframework.eventsourcing.eventstore.jpa.JpaEventStorageEngine.createTailToken(JpaEventStorageEngine.java:361) ~[axon-eventsourcing-4.6.2.jar:4.6.2]
    at org.axonframework.eventsourcing.eventstore.AbstractEventStore.createTailToken(AbstractEventStore.java:171) ~[axon-eventsourcing-4.6.2.jar:4.6.2]
    at org.axonframework.eventhandling.TrackingEventProcessor$WorkerLauncher.lambda$run$1(TrackingEventProcessor.java:1218) ~[axon-messaging-4.6.2.jar:4.6.2]
    at org.axonframework.common.transaction.TransactionManager.executeInTransaction(TransactionManager.java:47) ~[axon-messaging-4.6.2.jar:4.6.2]
    at org.axonframework.eventhandling.TrackingEventProcessor$WorkerLauncher.run(TrackingEventProcessor.java:1216) ~[axon-messaging-4.6.2.jar:4.6.2]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: DomainEventEntry is not mapped [SELECT MIN(e.globalIndex) - 1 FROM DomainEventEntry e]
    at org.hibernate.hql.internal.ast.QuerySyntaxException.generateQueryException(QuerySyntaxException.java:79) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.QueryException.wrapWithQueryString(QueryException.java:103) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:220) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:144) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:113) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:73) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:162) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.AbstractSharedSessionContract.getQueryPlan(AbstractSharedSessionContract.java:636) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:748) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    ... 14 common frames omitted
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: DomainEventEntry is not mapped
    at org.hibernate.hql.internal.ast.util.SessionFactoryHelper.requireClassPersister(SessionFactoryHelper.java:170) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.tree.FromElementFactory.addFromElement(FromElementFactory.java:91) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.tree.FromClause.addFromElement(FromClause.java:77) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.HqlSqlWalker.createFromElement(HqlSqlWalker.java:334) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.fromElement(HqlSqlBaseWalker.java:3782) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.fromElementList(HqlSqlBaseWalker.java:3671) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.fromClause(HqlSqlBaseWalker.java:746) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:602) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:339) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:287) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:276) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:192) ~[hibernate-core-5.6.14.Final.jar:5.6.14.Final]
    ... 20 common frames omitted

What else can I try?


Solution

  • I found the issue. Here's short summary how I fixed it.

    Addinng Entity Scan fixed the issue

    @SpringBootApplication
    @EnableSwagger2
    **@EntityScan(basePackageClasses = {DomainEventEntry.class, SagaEntry.class, TokenEntry.class})**
    public class EventEventApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EventEventApplication.class, args);
        }
    
    }
    
    

    Here's the Axon documentation https://docs.axoniq.io/reference-guide/axon-framework/spring-boot-integration#jpa-and-persistence-contexts

    TLDR : Long explanation I figured it out what was happening. I think this will help people who are getting same issue.

    Summary: Scenario 1 By default when you use

    <dependency>
                <groupId>org.axonframework</groupId>
                <artifactId>axon-spring-boot-starter</artifactId>
                <version>${axon-spring-boot-starter.version}</version>
     </dependency>
    

    the axon-server is included and app the wants to use the axon-server to store events. The Axon framework, will pick the first available JPA from you code and use it to store the events. Ex: If you use "JUST" H2 or MySQL database, the "Your tables" along with Axon-server event tables like Saga/Token Entry etc are saved in the same schema. This is when you don't create any AxonConfiguration and override to create a different Schema.

    Senario2: You have a table called "Product" in schema "ProductDB". On an event handler senario, your data will get saved in ProductDB/Product table. If you have not added any Axon configuration class, AxonAutoConfigure will save the Axon-server table also in "ProductsDB" ex: ProductsDB/Token-entry.

    If you want to keep Same DB but different Schemas ex: Products in ProductDB and Axon tables in AXONDB schema then you need to write AxonConfiguration, provide EntityManagerProvider, TokenStore and EventStorageEngine.

    Scenario 3 This was my scenario. My entity Product is saved in MONGODB and I wanted to save Axon events in MySQL. Now I added Axon configuration and provide the three amigo beans

    @Bean
        public EntityManagerProvider getEntityManagerProvider(){
            return new ContainerManagedEntityManagerProvider();
        }
    
       @Bean
       public TokenStore tokenStore(EntityManagerProvider entityManagerProvider) {
           return JpaTokenStore.builder().entityManagerProvider(entityManagerProvider)
                   .serializer(JacksonSerializer.defaultSerializer()).build();
       }
    
    @Bean
        public EventStorageEngine eventStorageEngine(Serializer serializer,
                                                     PersistenceExceptionResolver persistenceExceptionResolver,
                                                     @Qualifier("eventSerializer") Serializer eventSerializer,
                                                     EntityManagerProvider entityManagerProvider,
                                                     TransactionManager transactionManager) throws SQLException {
    
            JpaEventStorageEngine eventStorageEngine = JpaEventStorageEngine.builder()
                    .snapshotSerializer(serializer)
                    .persistenceExceptionResolver(persistenceExceptionResolver)
                    .eventSerializer(serializer)
                    .entityManagerProvider(entityManagerProvider)
                    .transactionManager(transactionManager)
                    .build();
    
            return eventStorageEngine;
        }
    
    

    Now the problem as I stated was that everything was fine, but in MySQL, I was not seeing domain_entry_event table and snapshot_entry_event tables. I assumed it was OK as I was using AXON-SERVER. The problem came when I restarted the application and I saw that Events were getting reapplied which was already executed to save Product entry in DB.

    Root Cause: When you use a Jpa then you must also override default configuration to tell axon to stop using Axon-server (exlude axon-server-connector from starter), then you must tell the persistent context to scan table @EntityScan(basePackageClasses = {DomainEventEntry.class, SagaEntry.class, TokenEntry.class})

    Addinng Entity Scan fixed the issue

    @SpringBootApplication
    @EnableSwagger2
    **@EntityScan(basePackageClasses = {DomainEventEntry.class, SagaEntry.class, TokenEntry.class})**
    public class EventEventApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EventEventApplication.class, args);
        }
    
    }