Declarative transaction management

Now, let's see how we can implement Spring declarative transaction management.

Our Messages App is so simple that it works fine without transaction management. For demonstration purpose, let's add another requirement to the /messages (POST) API. Let's say we want to have a report to show the statistics of hourly posted messages. We can calculate the statistics using an SQL query. Or we can update the statistics after saving a message and save it into a table that will hold this information in the database. Let's go with the second approach so that we can use declarative transaction management.

In practice, if our Message App had traffic like Disqus (https://disqus.com), you would probably want to take a different approach to update statistics, for example, send out an event to an MQ server and process the updating of statistics asynchronously.

 

To use declarative transaction management, we need to change AppConfig. Here are the changes:

...
@EnableTransactionManagement
public class AppConfig {
...
@Bean
public HibernateTransactionManager transactionManager() {
HibernateTransactionManager transactionManager =
new HibernateTransactionManager();
transactionManager.setSessionFactory(sessionFactory().getObject());
return transactionManager;
}
}

As you can see, we apply the @EnableTransactionManagement annotation to AppConfig and create a HibernateTransactionManager bean with the transactionManager() method. Inside this method, we set the same SessionFactory instance to this transaction manager.

By now, we've added transaction management to our app. Let's apply the @Transactional annotation to the MessageService.save() method. Here are the changes to the MessageService class:

public class MessageService {
...
@SecurityCheck
@Transactional
public Message save(String text) {
Message message = repository.saveMessage(new Message(text));
log.debug("New message[id={}] saved", message.getId());
updateStatistics();
return message;
}

private void updateStatistics() {
throw new UnsupportedOperationException("This method is not
implemented yet");
}
}

As you can see, we apply the @Transactional annotation to the save() method. And we refactor the method to call the updateStatistics() method that is not implemented and will throw UnsupportedOperationException. This will cause the transaction's rollback. And we add a debug message to the log so that you can see the message has been saved and is then rolled back.

If you restart the application and call the /messages (POST) API, you will see the following exception:

java.lang.ClassCastException: org.springframework.orm.jpa.EntityManagerHolder cannot be cast to org.springframework.orm.hibernate5.SessionHolder

This is because, by default, Spring Boot creates OpenEntityManagerInViewInterceptor, which registers EntityManager, an interface from JPA, to the current thread. And we're using Hibernate's SessionFactory in the repository. And, in the transaction advice, Spring will try to cast EntityManager to SessionFactory. To solve this issue, we can change our repository to use EntityManager instead of SessionFactory. Or we can turn this feature off by changing application.properties.

Here are the changes to application.properties:

logging.level.app.messages.MessageService=DEBUG
...
spring.jpa.open-in-view=false

We turn on the debug level logging of MessageService so that we can see the log in the output. And we set spring.jpa.open-in-view to false to overwrite the default setting to turn off the creation of OpenEntityManagerInViewInterceptor.

In Spring Boot 2, by default, a warning message shows up in the log when spring.jpa.open-in-view is not set to false. There are discussions regarding whether this should be turned on by default or not. If you're interested, you can check the discussion here: https://github.com/spring-projects/spring-boot/issues/7107.

Now, if you restart the application, you will see log information similar to the following in the console:

...
app.messages.SecurityChecker : Checking method security...
app.messages.MessageService : New message[id=6] saved
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.UnsupportedOperationException: This method is not implemented yet] with root cause
...

As you can see, the checkSecurity() advice was invoked. After that, we created a message and the exception was thrown as expected. However, if you check the messages table, message of id=6 is still there. It is not rolled back! What happened?

To find out what is wrong, first of all, let's review Spring's declarative transaction management, which is built on top of Spring AOP. As shown in Figure 3.9, Spring's transaction advisor creates transactions when the control flows in from the checkSecurity() advisor and either commits or rolls back the transaction after the control flows back to the transaction advisor. As mentioned earlier, a local transaction is associated with a JDBC connection. Therefore, when the transaction is created, a database connection is obtained from DataSource:

Figure 3.9: Transaction control flow

Let's take a look at the MessageRepository.saveMessage() method. Inside this method, we obtain a Hibernate session via sessionFactory.openSession(). The openSession() method will obtain a JDBC connection from DataSource. So, it seems that the connection obtained from this method is not the same one that the transaction advisor obtained.

This makes sense now. The MessageRepository.saveMessage() method uses a different JDBC connection to save the message and the transaction advisor cannot roll back a transaction of an unrelated connection.

Let's change the MessageRepository.saveMessage() method as follows:

public Message saveMessage(Message message) {
Session session = sessionFactory.getCurrentSession();
...
}

sessionFactory.getCurrentSession() will obtain the current session that is in the current Hibernate context which is shared with the transaction advisor. In this way, the transaction advisor can roll back the saved message upon the thrown exception.

Now, if you restart the application and call the /messages (POST) API, you can see the saved message is rolled back because of the exception thrown by the updateStatistics() method.

As mentioned earlier, Spring transaction management supports rollback rules. We can declare those exceptions where we want the transaction advisor to roll back the transaction and those exceptions where we don't want the rollback.

With declarative transaction management, we can declare the rules with the @Transactional annotation. Now, let's change it to make it not roll back for UnsupportedOperationException.

Here is the updated MessageService.save() method:

@Transactional(noRollbackFor = { UnsupportedOperationException.class })
public Message save(String text) {
...
}

Now, if you restart the application and call the API again, you will see that the message is saved in the messages table as expected.

We will discuss more about Spring transaction management when we create the TaskAgile application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.197.95