19

I have 2 repositories, one for mongodb (DocumentRepository) and the other for hibernate entity (EntityRepository)

I have a simple service:

 @Transactional
 public doSomePersisting() {
     try {
           this.entityRepository.save(entity);
           this.documentRepository.save(document);
     }
     catch(...) {
         //Rollback mongoDB here
     }
 }

Is it possible to rollback the mongoDB on the "//Rollback mongoDB here" line? I already got a rollback from the entity part (Transactional annotation)

AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277
Urbanleg
  • 6,252
  • 16
  • 76
  • 139

5 Answers5

14

MongoDB doesn't support transactions (at least not outside the scope of a single document). If you want to roll back changes you will need to handcraft that yourself. There are a few resources out there that describe ways of implementing your own transactions in Mongo if you really need them in certain circumstances. You could take a look at..

http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/

This is just an explanation of a pattern you could use. If you find that you absolutely need transactions in your application, you should consider whether MongoDB is a good fit for your needs.

dectarin
  • 986
  • 5
  • 15
  • 3
    You can leave your mongo operations for last (as you have in your example). So when it fails, it will rollback the spring ones before it. You'd want atomic operations for mongo usually, so this would keep your two data sources consistent. Unless you're using nested transactions, distributed transactions etc, none of that would work and you would have to compensate with a subsequent mongo update. – Zero Distraction Feb 23 '15 at 03:00
  • 6
    late to the party ;-) In Febuary 2018 MongoDB 4.0 was relased. It DOES support ACID transactions. From Spring Data 2.1 (Lovelace) you can use it with @Transactional annotation. So now it should be possible to perform real Two Phase Commit. You can also take a look at easier solution - ChainedTransactionManager in which can combine Mongodb TransactionManager and relationial database TransactionManager together – devstructor Jul 30 '18 at 14:29
4

Sorry for reposting my answer.

The earlier code was allowed to insert data into MongoDB even query exceptions throwing at data insertion into PostgreSQL(Using myBatis).

I have resolved the data Transaction issue between MongoDB and Relational database and @Transactional perfectly works by making these changes in the above code.

Solution for @Transactional Management.

Mongo Config class

@Configuration
public class MongoConfig extends AbstractMongoConfiguration{
    private static final Logger LOG = LoggerFactory.getLogger(MongoConfig.class);

    @Value("${spring.data.mongodb.database}")
    private String dbName;

    @Value("${spring.data.mongodb.host}")
    private String dbHost;

    @Value("${spring.data.mongodb.port}")
    private int dbPort;

    @Override
    public String getDatabaseName() {
        return dbName;
    }

    @Bean
    public MongoClient mongoClient(){
        return new MongoClient(dbHost, dbPort);
    }

    @Bean
    public MongoDbFactory mongoDbFactory(){
        return new SimpleMongoDbFactory(mongoClient(),dbName);
    }

    @Bean
    public MongoTemplate mongoTemplate() {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
        // Don't save _class to mongo
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(),mappingMongoConverter);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ON_ACTUAL_TRANSACTION);
        return mongoTemplate;
    }

    public MongoTemplate fetchMongoTemplate(int projectId) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
        // Don't save _class to mongo
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        MongoDbFactory customizedDBFactory = new SimpleMongoDbFactory(mongoClient(), dbName+"_"+projectId);
        MongoTemplate mongoTemplate = new MongoTemplate(customizedDBFactory,mappingMongoConverter);
        MongoTransactionManager mongoTransactionManager = new MongoTransactionManager(customizedDBFactory);
        return mongoTemplate;
    }

    @Bean
    public MongoTransactionManager mongoTransactionManager() {
        return new MongoTransactionManager(mongoDbFactory());
    }

}

Service class for Data insertion

@Service
@Component
public class TestRepositoryImpl implements TestRepository{
    private static final Logger LOG = LoggerFactory.getLogger(TestRepositoryImpl.class);


@Autowired MongoConfig mongoConfig;
@Autowired MongoTemplate mongoTemplate;
@Autowired MongoTransactionManager mongoTransactionManager;

@Autowired UserService userService;

@Override
@Transactional
public void save(Test test){
    int projectId = 100;
    if (projectId != 0) {
        mongoTemplate = mongoConfig.fetchMongoTemplate(100);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ALWAYS);
    }
    mongoTemplate.insert(test);
    IdName idName = new IdName();
    idName.setName("test");
    mongoTemplate.insert(idName);
    User user = new User();
    user.setName("Demo");
    user.setEmail("srini@abspl.in");
    user.setPassword("sdfsdfsdf");
    userService.save(user);
    }
 }

POM.XML

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.abcplusd.sample.mongoapi</groupId>
  <artifactId>sample-mongo-api</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>Sample Spring Boot Mongo API</name>
  <description>Demo project for Spring Boot Mongo with Spring Data Mongo</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-mongodb</artifactId>
      <version>2.1.0.RELEASE</version>
      <exclusions>
        <exclusion>
          <groupId>org.mongodb</groupId>
          <artifactId>mongo-java-driver</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-commons</artifactId>
      <version>2.1.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.mongodb</groupId>
      <artifactId>mongo-java-driver</artifactId>
      <version>3.8.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>42.2.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.5</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
Srini
  • 823
  • 5
  • 10
  • 29
2

With MongoDb 4.0.x you can use transactions. If you use a version below you have to implement a two phase commit.

NB: MongoDb allows you to use transactions only if you have a ReplicaSet.

In order to use a transaction for both JPA and MongoDb you have to use a ChainedTransactionManager. The process is :

  • create Jpa Transaction manager
  • create MongoDb transaction manager
  • create ChainedTransactionManager which will use the two above

My conf looks like this (I don't use spring boot, but it should be equivalent) :

Jpa configuration

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories("com....")
public class HibernateConfig {

    //define entity manager, data source and all the stuff needed for your DB

    @Bean("jpaTransactionManager")
    public JpaTransactionManager transactionManager() throws NamingException { 

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        //transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());

        return transactionManager;
    }
}

MongoDb configuration

@Configuration
@EnableMongoRepositories(basePackages = "com....")
public class MongoDbConf extends AbstractMongoClientConfiguration {

    private final Environment environment;

    @Autowired
    public MongoDbConf(Environment environment) {
        this.environment = environment;
    }

    @Override
    public MongoClient mongoClient() {
        String connectionString = environment.getProperty("mongodb.connectionString");

        if(StringUtils.isBlank(connectionString))
            throw new IllegalArgumentException("No connection string to initialize mongo client");

        return MongoClients.create(
                MongoClientSettings.builder()
                        .applyConnectionString(new ConnectionString(connectionString))
                        .applicationName("MY_APP")
                        .build());
    }

    @Override
    protected String getDatabaseName() {
        return environment.getProperty("mongodb.database", "myDB");
    }

    @Bean("mongoDbTransactionManager")
    public MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
        return new MongoTransactionManager(dbFactory);
    }
}

ChainedTransactionManager configuration

@Configuration
public class ChainedTransactionConf {

    private MongoTransactionManager mongoTransactionManager;
    private JpaTransactionManager jpaTransactionManager;

    @Autowired
    public ChainedTransactionConf(MongoTransactionManager mongoTransactionManager, JpaTransactionManager jpaTransactionManager) {
        this.mongoTransactionManager = mongoTransactionManager;
        this.jpaTransactionManager = jpaTransactionManager;
    }

    @Bean("chainedTransactionManager")
    public PlatformTransactionManager getTransactionManager() {
        ChainedTransactionManager transactionManager = new ChainedTransactionManager(jpaTransactionManager, mongoTransactionManager);
        return transactionManager;
    }

}

Example of a mongoDb repo

@Service
public class MongoDbRepositoryImpl implements MongoDbRepository {

    private static final Logger logger = Logger.getLogger(MongoDbRepositoryImpl.class);

    //MongoOperations will handle a mongo session
    private final MongoOperations operations;

    @Autowired
    public MongoDbRepositoryImpl(MongoOperations operations) {
        this.operations = operations;
    }

    @Override
    public void insertData(Document document) {
        MongoCollection<Document> collection = operations.getCollection("myCollection");
        collection.insertOne(document);
    }

Using transaction in your service

@Service
public class DocumentServiceImpl implements DocumentService {

    private final MongoDbRepository mongoDbRepository;
    private final JpaRepository jpaRepository;

    @Autowired
    public DocumentServiceImpl(MongoDbRepository mongoDbRepository,JpaRepository jpaRepository) {
        this.mongoDbRepository = mongoDbRepository;
        this.jpaRepository = jpaRepository;
    }

    @Override
    @Transactional("chainedTransactionManager")
    public void insertNewDoc(Map<String,Object> rawData) {
        //use org.springframework.transaction.annotation.Transactional so you can define used transactionManager
        //jpaRepository.insert...
        Document mongoDoc = new Document(rawData);
        mongoDbRepository.insertData(mongoDoc)

        //you can test like this : breakpoint and throw new IllegalStateException() 
        //to see that data is not commited 
    }
1

MongoDB v4.x.x works perfectly with @Transactional, they have explicit support for this by using the following dependency and repository :-

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-releasetrain</artifactId>
    <version>Lovelace-M3</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

And a MongoTransactionConfig class:-

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;

@Configuration
@EnableMongoRepositories(basePackages = "com.airtel.africa.caxr.repository")
public class MongoTransactionConfig extends AbstractMongoClientConfiguration {

    @Value("${spring.data.mongodb.host}")
    private String host;
    @Value("${spring.data.mongodb.port}")
    private String port;
    @Value("${spring.data.mongodb.database}")
    private String database;

    @Bean
    MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
        return new MongoTransactionManager(dbFactory);
    }

    @Override
    protected String getDatabaseName() {
        return database;
    }
    @Override
    public MongoClient mongoClient() {
        String connectionString = "mongodb://"+host+":"+port;

        return MongoClients.create(MongoClientSettings.builder()
            .applyConnectionString(new 
        ConnectionString(connectionString)).build());
    }
}

And here I'm using mongo with kafka as a 1 transaction so if any checked or unchecked exception occurs here then mongo transaction should be rolled back so I used @Transactional(rollbackFor = Exception.class):-

@Transactional(rollbackFor = Exception.class)
public void receiveInEventRequest(TransactionDto transactionDto) throws 
    InterruptedException, ExecutionException {
    // db insert    
    TransactionRequest transactionRequest = requestDbDumpService.dumpToDb(transactionDto);
    // kafka insert
    ListenableFuture<SendResult<String, TransactionDto>> kafkaResult = kafkaTemplate.send(kafkaProducerQueueName, “ID”, transactionDto);
    SendResult<String, TransactionDto> kafkaSendResult = kafkaResult.get();
}
Rohan Dodeja
  • 296
  • 1
  • 5
  • 18
1

If anyone is in need of transactional support for reactive style spring boot and MongoDb integration then please go through the answer here

kakabali
  • 3,824
  • 2
  • 29
  • 58