What is a transaction? It is a set of related operations, all of which must be performed correctly without error. Otherwise, all the operations are canceled. An example of where this mechanism is used is a database.

In this topic, we are going to explore how to manage transactions in Spring using the @Transactional annotation and then look at some ways to manage transactions.

Spring Boot + @Transactional annotation

There is a very simple declarative way to manage transactions in Spring. You just put @Transactional from the org.springframework.transaction.annotation package in definitions of class, interface, or method, and all operations inside the annotated unit will be performed in a transaction. Note that if you add this annotation to the class Spring applies it to all public methods inside.

It is important to keep in mind that the annotation will work only on public methods. The transaction processing mechanism will not work for a private or protected method.

If an exception occurs during the operation and it is not caught inside the method, the transaction will be canceled and all data will return to the state in which it was before the start of the transaction. It is like a rollback operation in DB terms.

For example, we have PersonRepository which extends the CrudRepository interface. It allows our repository to interact with the database using special methods.

public interface PersonRepository extends CrudRepository<Person, Long> {
}

For example, we have a PersonService service with the doTransaction() method, which creates an entity, saves it to the database, then updates its name.

@Service
public class PersonService {

    private final PersonRepository repository;

    @Autowired
    public PersonService(PersonRepository repository) {
        this.repository = repository;
    }

    public void doTransaction() {
        var person = new Person();
        person.setName("Mike");
        repository.save(person); // insert
        person.setName("Bob");
        repository.save(person); // update
    }
}

Now let’s add an error before updating our entity.

public void doTransaction() {
    var person = new Person();
    person.setName("Mike");
    repository.save(person); 

    if (person.getId() > 0) {
        throw new RuntimeException();
    }

    person.setName("Bob");
    repository.save(person);
}

When we run the code above, a new record will be added to the table, but the next update will not occurOur data will not be consistent. To avoid this situation, we just add the @Transactional annotation to the method.

@Transactional
public void doTransaction() {
    var person = new Person();
    person.setName("Mike");
    repository.save(person);

    if (person.getId() > 0) {
        throw new RuntimeException();
    }

    person.setName("Bob");
    repository.save(person);
}

In this case, the first query will be executed and then canceled, and no new records will appear in the table in the database.

The annotation supports several parameters. One of them is a readOnly flag. You can use it like this: @Transactional(readOnly = true) . It is just a “the transaction should be read-only” hint for the transaction manager. The default value is false. However, there is no guarantee that, for example, an update operation won’t occur if we set a readOnly flag.

Note that rollback works for unchecked runtime exceptions by default. We can change it using the rollbackFor and noRollbackFor parameters.

Keep in mind that if you add the @Transactional annotation to the whole class, only its public methods will be affected.

Declarative and programmatic transaction management

Spring supports two types of transaction management:

  • Programmatic Transaction Management. You must manage your transactions through programming. This method is flexible enough but difficult to maintain.
  • Declarative Transaction Management. Allows us to manage transactions through configuration. To do it, we often use annotations.

As we mentioned in the previous section, using the @Transactional annotation is a declarative way of managing transactions. Let’s see an example of how to manually configure transaction management. We have PlatformTransactionManager class for these purposes. We use it to create, commit, or roll back transactions.

@Service
public class PersonService {

    private final PersonRepository repository;
    private final PlatformTransactionManager trManager;

    @Autowired
    public PersonService(PersonRepository repository, 
                         PlatformTransactionManager transactionManager) {
        this.repository = repository;
        this.trManager = transactionManager;
    }

    public void doTransaction() {
        TransactionDefinition trDefinition = new DefaultTransactionDefinition();
        TransactionStatus trStatus = trManager.getTransaction(trDefinition);

        try {
            var person = new Person();
            person.setName("Mike");
            repository.save(person); // insert
            person.setName("Bob");
            repository.save(person); // update
            trManager.commit(trStatus);
        } catch (Exception ex) {
            ex.printStackTrace();
            trManager.rollback(trStatus);
        }
    }
}

The DefaultTransactionDefinition exposes various setter methods to configure the transaction accordingly. Also, TransactionStatus was created . It will be used by the transaction manager to commit or roll back the transactions.

The main advantage of this approach is that the boundaries of the transaction are obvious in the code. But there are some disadvantages as well. One of them is that you should write repetitive code. Also, any mistake might have a very big impact.

In most cases, declarative transaction management provides the same capabilities as the programmatic approach, while being less verbose and easier to implement. If setting an annotation is not enough and you want to control the transaction management explicitly, the programmatic way might be your best choice. But remember that it is not necessary to use only one of them. You can use both in one project.

Conclusion

In this topic, we learned how to use Spring with the @Transactional annotation to maintain data consistency. We also talked about the readOnly parameter and looked at two ways of transaction management: programmatic and declarative. You can use the first one If you want to implement specific behavior. But in most cases, the declarative way is enough.

Leave a Reply

Your email address will not be published.