Transactions: A Spring approach

Transactions: A Spring approach

Great applications need to be secure about the integrity of the data they handle. Many times, operations (functionalities) must be composed of several steps, which must be executed together for the whole operation to be successful. This set of operations is called a transaction.

A Transaction is a unit of execution, which can have several operations that must be executed together and the success of this unit depends on the success of all the operations involved.

Making it easier, a classic example can be shown: a bank transfer. If a value is to be transferred from an X account to a Y account, some operations must be done:

  1. Check your Account X balance
  2. Debit the amount of Account X
  3. Credit the amount to Account Y

Thus, the transfer should only be successful if all operations are successful. If an error happens when checking the balance, withdrawing the amount from the account or credit in the other account, the whole operation should not be performed. Otherwise, inconsistencies could occur, such as leaving Account X negative or withdrawing money from X but not crediting Y.

To ensure that a transaction is executed with success, it must comply with the ACID properties:

  • Atomicity: All operations must modify the state of the data with success or none of them;
  • Consistency: A transaction must preserve data integrity;
  • Isolation: If simultaneous transactions exist, each one must be able to operate in isolation, unaware of each other's existence, and finish with success;
  • Durability: Finished, a transaction should allow data to be always available, even if the application that started and finished it fails.

To ensure that applications can perform their transactions in a secure way and comply with ACID properties, languages and frameworks provide their APIs. However, all of these APIs use two key concepts to ensure that ACID properties are insured: Propagation and Isolation.

Propagation is the ability to configure how operations (methods) encapsulate transactions, whether logical or physical. Logical transactions are those that each operation (method) in isolation defines. However, many times several methods work together and the combination of these logical transactions generates a physical transaction. That is, logical transactions relate to how the application handles transactions and physical transactions relate to how the database views logical transactions.

Isolation is the ability to configure visibility between simultaneous transactions. That is, if a particular transaction modifies (inserts, updates, or deletes) some data, we can inform this transaction whether its modifications are available or not for other transactions.

To exemplify how to apply propagation and isolation, I will use the Spring framework. In the following sections, I will present the main parts of your API and some example scenarios. The application provided is more complete than the codes presented here.

@Transactional

This annotation indicates that the operation (method) will be transacted. It can be used in both methods or classes. The most common uses are in methods. This way, you can define transactions more granular way. If used in the class, it indicates that all methods will be transacted, which is not always necessary. Its configuration attributes are:

  • isolation label
  • noRollbackFor
  • noRollbackForClassName
  • propagation
  • readOnly
  • rollbackFor
  • rollbackForClassName
  • timeout
  • timeoutString
  • value

We will explore the most used ones: propagation and isolation. Before starting the examples, let's show the enums that Spring provides.

Enum Propagation

This Enum provides settings for how the transaction should behave (propagate). Its attributes and behaviors are:

  • MANDATORY: uses the current transaction and throws an exception if one not exist;
  • REQUIRED: joins the current transaction. If one does not exist, create it;
  • NESTED: creates a transaction parallel to the pre-existing transaction. If one does not exist, create it;
  • NEVER: does not allow transaction execution. If a transaction exists, an exception will be thrown;
  • NOT_SUPPORTED: does not allow transaction execution. Suspends the current transaction, if one exists;
  • REQUIRES_NEW: creates a new transaction and suspends the current;
  • SUPPORTS: joins the current transaction. If one does not exist, it does not create it;

Enum Isolation

This Enum provides configurations of how the transactions see each other. Its attributes and behaviors are:

  • DEFAULT: uses the isolation defined directly in the database;
  • READ_COMMITED: indicates that a transaction cannot read data that has not yet been committed;
  • READ_UNCOMMITED: indicates that a transaction can read data that has not yet been committed;
  • REPEATABLE_READ: indicates that if a transaction initially reads a set of rows from the database, the next several readings must have the same result. If rows were updated, deleted, or inserted and are expected to appear in this reading, an exception is thrown;
  • SERIALIZABLE: indicates that the transaction sees only the data that belongs to it. That is, even if other data has committed in other transactions, they will not be visible. It happens because they were not manipulated in the current transaction. It is the most restrictive isolation.

Examples

All the following examples use two tables: tb_student and tb_grade. These have the columns id, tx_name, nr_registration and id, id_student, nr_grade, ds_discipline, in order. The database used was PostgreSQL version 13. An application with all the examples was built in SpringBoot 2.4.1 and SpringData 2.4.2. To see the results, just start the application and view the displayed logs. Examples 1 to 7 is about propagations. Examples 8 to 11 are about isolations. Example 12 is an example of one of the possible mixtures of propagation and isolation.

Example 1

Using @Transactional without attributes.

@Transactional
public void execute() {

 try {
  this.studentService.save(student);
  this.gradeService.saveGrades(grades);
 } catch (FakeErrorException e) {
  throw e;
 }

}

public void save(Student student) {
 this.studentRepository.save(student);
}

@Transactional
public void saveGrades(List<Grade> grades) {
 this.gradeRepository.saveAll(grades);
 throw new FakeErrorException(...);

}

In this code, only the annotation was used. This indicates that Spring should use the default values for propagation and isolation, REQUIRED and DEFAULT, respectively. Thus, when execute runs, it will create a transaction, which will be used in studentService.save and gradeService.saveGrades. However, gradeService.saveGrades throws an exception. Then, neither the student nor his grades are saved, because a rollback for both was done.

Example 2

Using Propagation.MANDATORY.

public void execute() {
 try {
  this.studentService.saveMandatory(student);
 } catch (TransactionException e) {
  e.printStackTrace();
 }
}

@Transactional(propagation = Propagation.MANDATORY)
public void saveMandatory(Student student) {
 this.studentRepository.save(student);

}

In this code, an exception will be thrown, because the initial method (execute) did not start a transaction. It happens because saveMandatory in studentService is required (MANDATORY) the existence of one.

Example 3

Unfortunately for NESTED transactions, we will not have an executable example. This is because Hibernate does not support this type of transaction. We use SpringData and Hibernate, so it's impossible to present a real code about this propagation. It would be necessary to show codes with JDBCTemplate, which is not the focus of this article. This class is responsible for providing a native JDBC connection, which supports NESTED transactions. However, I will present below a “fake example” of how this type of transaction behaves.

@Transactional
public void execte() {
 this.studentService.save(student);
 this.gradeService.saveNested(grades);
}

public void save(Student student) {
 this.studentRepository.save(student);
}

@Transactional(propagation = Propagation.NESTED)
public void saveNested(List<Grade> grades) {
 this.gradeRepository.saveAll(grades);
 throw new FakeErrorException(...);

}

In this code, even if saveNested throws an exception, the student will be saved. This is because the saveNested method started a subordinate transaction (NESTED) to the transaction initiated by execute. To do this, a savePoint is created right after the student has been saved. Then, when the error was thrown, the rollback was made only as far as the savePoint. This transaction is very useful when you have a large amount of data (thousands of rows) and you don't want to do a full rollback if an error happens. Thus, partial rollbacks can be done and be able to commit parts of the data to the database. Although it looks a lot like REQUIRES_NEW, they are completely different behaviors.

Example 4

Using Propagation.NEVER.

@Transactional
public void execute() {
 try {
  this.studentService.saveNever(student);
 } catch (TransactionException e) {
  e.printStackTrace();
 }
}

@Transactional(propagation = Propagation.NEVER)
public void saveNever(Student student) {
 this.studentRepository.save(student);

}

In this code, an exception will be thrown, because a transaction was started by execute, but saveNever in studentService does not allow the existence of one.

Example 5

Using Propagation.NOT_SUPPORTED.

@Transactional
public void execute() {
 try {
  this.studentService.save(student1);
  this.studentService.saveNotSupported(student2);
 } catch (FakeErrorException e) {
 }
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveNotSupported(Student student) {
 this.studentRepository.save(student);
 throw new FakeErrorException(...);

}

In this code, saveNotSupported threw an exception, but student2 was saved. This is because the transaction initiated at execute has been paused. Then, when the error was thrown, student1 and student2 had already saved in the database.

Example 6

Using Propagation.REQUIRES_NEW.

@Transactional
public void execute() {
 try {
  this.studentService.saveRequiredNew(student);
  this.gradeService.saveGrades(grades);
 } catch (FakeErrorException e) {
  throw e;
 }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveRequiredNew(Student student) {
 this.studentRepository.save(student);
}

@Transactional
public void saveGrades(List<Grade> grades) {
 this.gradeRepository.saveAll(grades);
 throw new FakeErrorException(...);

}

In this code, saveGrades throwing an error, but the student was saved. This is because saveRequiredNew in studentService starts a new transaction and pause the current. That is, the error thrown in the execute transaction, which is the same as in saveGrades does not interfere in the transaction started in saveRequiredNew. In this way, the rollback was only made for saveGrades.

Example 7

Using Propagation.SUPPORTS.

public void execute() {
 executeWithoutTransaction();
 executeWithTransaction();
}

protected void executeWithoutTransaction() {
 this.studentService.saveSupports(student);
}

@Transactional
protected void executeWithTransaction() {
 this.studentService.saveSupports(student);
}

@Transactional(propagation = Propagation.SUPPORTS)
public void saveSupports(Student student) {
 this.studentRepository.save(student);

}

In this code, each student is saved without any problems. execute did not start a transaction, so executeWithoutTransaction executes the saveSupports method non-transactionally. The executeWithTransaction method creates a transaction, so now saveSupports executes transactionally way.

Example 8

Using Isolation.READ_COMMITED.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void execute() {
 this.studentService.save(student);
 this.searchService.findStudentByRegistrationNumber(student
  .getRegistrationNumber());

}

In this code, the student was saved in save. However, the execute method has not finished yet and his transaction has not yet been committed. Therefore, as the isolation is READ_COMMITTED, the findStudentByRegistrationNumber method cannot get the student. However, if a query is made after executing execute, the student will be found.

Example 9

Using Isolation.READ_UNCOMMITED.

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void execute() {
 this.studentService.save(student);
 this.searchService.findStudentByRegistrationNumberWithoutTransaction(student
  .getRegistrationNumber());

}

In this code, the student is saved in save and is already available at findStudentByRegistrationNumberWithoutTransaction, because even though the execute method has not yet finished and the commit has been done, the READ_UNCOMMITTED isolation allows your reading.

Example 10

Using Isolation.REPEATABLE_READ.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void execute() {
 try {
  this.searchService.listAllStudents();
  List<Student> students = this.searchService.findAllStudents();
  Student student = students.get(0);
  student.setRegistrationNumber("99999999-9");
  this.studentService.saveRequiredNew(student);
  this.searchService.findAllStudents();
 } catch (CannotAcquireLockException e) {
  throw e;
 }

}

In this code, the first reading of all students is done using the findAllStudents method. After that, an update is made on a student. However, when trying to read all students again, an error occurs. This is because the REPEATABLE_READ isolation identified that the reading would no longer be the same, as an updated row was returned, which was modified by a concurrent transaction (studentService.saveRequiredNew).

Example 11

Using Isolation.SERIALIZABLE.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void execute() {
 this.searchService.listAllStudents();
 this.studentService.saveRequiredNew(student);
 this.searchService.listAllStudents();

}

In this code, students who were shown the first time will be obtained exactly the same the second time. Even with the created student in studentService.saveRequiredNew, it will not appear in the second query. This is because this new student was created by a new transaction and not by the one that was being executed (execute method), with SERIALIZABLE isolation. This behavior occurs because this is the most restrictive isolation and limits its scope only to the current transaction.

Example 11

In this final example, a combination of propagation and isolation is presented. There are many possibilities for using the two together, so this example will only show one of them.

@Transactional
public void execute() {
 this.studentService.save(student);
 this.gradeService.save(grade);
}

@Transactional(propagation = Propagation.SUPPORTS, isolation = Isolation.READ_UNCOMMITTED)
public void save(Grade grade) {
 this.gradeRepository.save(grade);

}

In this code, Propagation.SUPPORTS and Isolation.READ_UNCOMMITED were used. That means that the gradeService.save method joined the execute transaction and even though it has not yet committed its data, these are available. If they were not, an integrity error would happen, as the grades would belong to a student not yet saved in the database.

Conclusion

This article showed that the concept of transaction is not complex and Spring has a very intuitive API for working with them. Only propagation and isolation have been explained, but the @Transational annotation has other attributes that can be used if necessary. I advise you to read them.

All the examples presented here are available on the Git link below:

https://github.com/thiagoleitecarvalho/SpringTransactionExamples

References

Silberschartz, Abraham. Sistema de Banco de Dados. Avi Silberschartz, Henry F. Korth, S.Sudarschan; Tradu??o Daniel Vieira. Rio de Janeiro: Elsevier 2012

https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Isolation.html

Mahmoud Mohammadi

Technical lead at blu bank

3 年

good ?? . would you make an article for time out spring transaction ?

回复
Joao Vale

Software Engineer | Technical Leader | Professor | Backend Developer | OCP 11 | Spring Boot | Spring| Java Specialist

4 年

Nice article and a good summary. :)

要查看或添加评论,请登录

Thiago Leite e Carvalho的更多文章

  • Conversando sobre acoplamento

    Conversando sobre acoplamento

    Quando nós iniciamos a programar com linguagens Orientadas a Objetos, uma famosa frase é apresentada: “Alta coes?o e…

    5 条评论
  • Talking about coupling

    Talking about coupling

    When we start programming with Oriented Object languages, a famous phrase is presented to us: “High cohesion and Low…

    1 条评论
  • Are you logging?

    Are you logging?

    - Are you logging? - Yes. I'm using a "sysout", "print" or "console.

  • Building good APIs

    Building good APIs

    Knowing how is the architect of a REST API is important. I could talk about this in this article, but my friend Bruno…

    3 条评论
  • Exceptions: What is?, How to use? and When to use? - Part I

    Exceptions: What is?, How to use? and When to use? - Part I

    There's nothing worse when we use a software and an error occurs what shows us a message we can't understand…

    1 条评论

社区洞察

其他会员也浏览了