Using @DataMongoTest currently does not enable auto-configuration for transactions which means that tests for transactional behavior, e.g. data not persisted on transaction rollback, will not work out of the box. Adding @ImportAutoConfiguration(TransactionAutoConfiguration.class) (or the equivalent reactive variant) makes this work.

Original StackOverflow post: https://stackoverflow.com/questions/60178310/spring-data-mongodb-transactional-isnt-working/60184283?noredirect=1#comment106470903_60184283

Comment From: mp911de

Note that transactions require a ReplicaSet setup. It makes sense to enable transactional interceptors but we should leave the decision, whether a Mongo Transaction manager gets registered up to the user.

Comment From: snicoll

Thanks @mp911de.

@odrotbohm Spring Boot does not currently auto-configure MongoTransactionManager as far as I can see so I think we need to revisit this a bit. I am not sure adding @Transactional on @DataMongoTest is consistent if we don't auto-configure the necessary infrastructure that is required for it to operate properly.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: odrotbohm

I agree. I guess the question is then two-fold:

  1. How do we make the difference in bootstrapped setup obvious to users. @DataJpaTests includes transactional auto-configuration, @DataMongoTests doesn't. Shall we add an explicit note about that in the annotation's Javadoc?
  2. What's the most idiomatic to add transactional behavior to the tests? Could we maybe use the availability of a MongoDbTransactionManager in the ApplicationContext to automatically enable transactions? That would at least let users properly run into the limitation outlined by Mark which then brings them to the topic they actually need to solve.

WDYT?

Comment From: snicoll

We've discussed this at the team meeting and we decided to do the following:

  • Add the necessary auto-configuration so that transaction management can work (transaction interceptors)
  • Add a @DataMongoTest sample test that configures a transaction manager (with replica set) to showcase and validate the necessary steps to enable this feature

In particular we decided not to:

  • Add @Transactional to @DataMongoTest as this is an opt-in behaviour as discussed above and adding it would give the false promise things happen automatically
  • Call it out in the javadoc of @DataMongoTest but check that this is explicitly documented for test slices where transaction happens automatically. We feel that mentioning explicitly features that are not supported out-of-the-box would not be consistent with all the places where we don't do anything and it isn't called out explicitly either.

Comment From: odrotbohm

Nice, thank you everyone!

Comment From: wilkinsona

The hardest part of this is configuring Mongo itself to use a replica set and then waiting for Mongo to have initialised such that it's in a state where a client session can be created. Using embedded Mongo, that looks like this:

@TestConfiguration(proxyBeanMethods = false)
static class MongoCustomizationConfiguration {

    private static final String REPLICA_SET_NAME = "rs1";

    @Bean
    public IMongodConfig embeddedMongoConfiguration(EmbeddedMongoProperties embeddedProperties) throws IOException {
        IMongoCmdOptions cmdOptions = new MongoCmdOptionsBuilder().useNoJournal(false).build();
        return new MongodConfigBuilder().version(Version.Main.PRODUCTION)
                .replication(new Storage(null, REPLICA_SET_NAME, 0)).cmdOptions(cmdOptions)
                .stopTimeoutInMillis(60000).build();
    }

    @Bean
    MongoInitializer mongoInitializer(MongoClient client, MongoTemplate template) {
        return new MongoInitializer(client, template);
    }

    static class MongoInitializer implements InitializingBean {

        private final MongoClient client;

        private final MongoTemplate template;

        MongoInitializer(MongoClient client, MongoTemplate template) {
            this.client = client;
            this.template = template;
        }

        @Override
        public void afterPropertiesSet() throws Exception {
            List<ServerDescription> servers = this.client.getClusterDescription().getServerDescriptions();
            assertThat(servers).hasSize(1);
            ServerAddress address = servers.get(0).getAddress();
            BasicDBList members = new BasicDBList();
            members.add(new Document("_id", 0).append("host", address.getHost() + ":" + address.getPort()));
            Document config = new Document("_id", REPLICA_SET_NAME);
            config.put("members", members);
            MongoDatabase admin = this.client.getDatabase("admin");
            admin.runCommand(new Document("replSetInitiate", config));
            Awaitility.await().atMost(Duration.ofMinutes(1)).until(() -> {
                try (ClientSession session = this.client.startSession()) {
                    return true;
                }
                catch (Exception ex) {
                    return false;
                }
            });
            this.template.createCollection("exampleDocuments");
        }

    }

}

MongoInitializer is responsible for initialising the replica set, waiting for Mongo to be ready for use, and then creating the document. Document creation is done here as it has to be done outside of a transaction.

The IMongodConfig bean is required as the use of transactions requires the journal to be enabled:

IMongoCmdOptions cmdOptions = new MongoCmdOptionsBuilder().useNoJournal(false).build();

We also increate the stop timeout as Mongo seems to take longer to stop when a replica set has been configured and embedded Mongo's default is too short.

If you had Mongo pre-configured with a replica set via some other means, using transactions will now be pretty straightforward. You add @Transactional to your tests and provide a transaction manager either in your tests or, more likely, your main application code:

@Bean
MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) {
    return new MongoTransactionManager(dbFactory);
}