A preface,
Hello guys, I’m beer bear, today I want to talk to you about some problems in Spring involving database transactions.
Spring provides developers with a way to use declarative transactions by marking the @Transactional annotation on methods to open transactions.
We all know that we want transaction control when business code is manipulating data.
For example, when writing the business code of the merchant selling things, the logic of the code is that the merchant creates an order (the order information is inserted into the database), and then the money is added to his account (the money in the database is increased). If the latter operation fails, then the former operation must not succeed, which is when the rollback of the transaction is used.
Although most backend developers are familiar with this concept, there are still some errors when using the @Transactional annotation.
A few days ago, while doing a code review of some new students in the company, I saw some errors in their Spring project regarding the @Transactional annotation. In order to correct their mistakes at the same time can not help but think of themselves also fell into these pits, (* * *) Blue
Therefore, I want to make a guide to the use of this annotation ~ and share it with the community.
This article introduces some of the common mistakes that can be made about @Transactional in ordinary business development, along with some examples of the wrong code. For each error type, explain why, and show the correct posture to use the @Transactional annotation. Let’s take a look!
Two, some preparation
2.1 database
We define a GOOds_stock inventory table in the database and give it some initial data:
The current inventory of good_0001 is 10.
2.2 Spring Boot + Mybatis
We use Mybatis to destock our Java methods and annotate them with @Transactional annotations to see if the Transactional annotation invalidates the transaction and rolls back when it fails.
The project structure is as follows:
We will use Swagger to call the interface in the Controller layer, where we will call the specific business code in the Service layer GoodsStockServiceImp, the destocking operation.
If the business code is successfully executed, the inventory of the commodity becomes 0:
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.beerbear.springboottransaction.dao.GoodsStockMapper">
<update id="updateStock">
update goods_stock set stock = stock - 10
</update>
</mapper>
Copy the code
Throw an exception
3.1 Exceptions propagate methods that do not make the @Transactional mark
Many times, in real business development, you want an interface to return a fixed instance of a class — this is called a uniform return result. In this article, the Result class is used as the uniform return Result. See the attached code for details.
It is then possible to return a Result object directly in the method of a Service for convenience. To avoid being affected by an exception and not being able to return the Result set, a try-catch statement is used, which will be caught when the business code throws an exception in error. Writes the exception information to the relevant field of Result and returns it to the caller. An example of this type is given below:
The Controller layer:
@Controller
@RestController
@API (tags = "Test transaction is working ")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {
@Autowired
private GoodsStockService goodsStockService;
/** * create by: Beer Bear * description: * create time: 2021/7/25 21:38 */
@GetMapping("/exception/first")
@apiOperation (value = "first method about exception, could not be rolled back ", notes =" No rollback because exception was not found by transaction ")
@ResponseBody
public Result firstFunctionAboutException(a){
try{
return goodsStockService.firstFunctionAboutException();
}catch (Exception e){
return Result.server_error().Message("Operation failed:"+e.getMessage()); }}}Copy the code
Methods in Service:
@Autowired
private GoodsStockMapper goodsStockMapper;
@Override
@Transactional
public Result firstFunctionAboutException(a) {
try{
log.info("Destocking begins.");
goodsStockMapper.updateStock();
if(1= =1) throw new RuntimeException();
return Result.ok();
}catch (Exception e){
log.info("Inventory reduction failed!" + e.getMessage());
return Result.server_error().Message("Inventory reduction failed!"+ e.getMessage()); }}Copy the code
In the try block firstFunctionAboutException method, a RuntimeException will be thrown exception, but it can roll back? We might as well see through the experiment:
Call interface with Swagger:
After the interface is called, the transaction should be rolled back and the inventory quantity does not go to zero, but the result is:
In order to save space, these screenshots will no longer appear in the following paragraphs, but will be replaced with text
Clearly the transaction is not rolled back. We all know that a transaction is rolled back when an exception is thrown because of an error during execution. In this case, the exception is caught by the method itself, the exception is not detected by the transaction, so there is no rollback.
We will remove the try-catch statement from the service.
@Override
@Transactional
public void secondFunctionAboutException(a) {
log.info("Destocking begins.");
goodsStockMapper.updateStock();
if(1= =1) throw new RuntimeException();
}
Copy the code
This allows the transaction to be rolled back. (In this case, how to handle the exception? You can not directly declare an exception, it is easy to put the exception on the Controller layer to handle it.)
Here is a summary of the first pit avoidance guide:
When mark
@Transactional
When an exception occurs in an annotated method, the transaction is not rolled back if the exception is not propagated outside the method; Conversely, the transaction is rolled back only if the exception is propagated outside the method.
3.2 No rollback when an exception is thrown
Now we all know that when a program fails to execute and throws an exception, the desired rollback can be achieved simply by not processing the exception and letting it break the @Transactional method.
But is this really the case? Let’s look at another case:
@Override
@Transactional
public void thirdFunctionAboutException(a) throws Exception {
log.info("Destocking begins.");
goodsStockMapper.updateStock();
if(1= =1) throw new Exception();
}
Copy the code
In fact, transactions in this method are not rolled back.
This is one of the most common mistakes we make in real development, thinking that if we throw an exception, it will be rolled back, only to be slapped in the face by reality.
But I don’t think it is a shame, because when we use a tool, we may not have the energy and ability to learn some of its principles at the beginning, so we fall into some pits that are not easy to find. As long as we insist on learning behind, we will slowly fill these pits, and we will become stronger and stronger.
Ok, anyway, why doesn’t the transaction roll back here? We will this method compared with secondFunctionAboutException one of the above, find the difference between just a RuntimeException and Exception. This is true because Spring’s @Transactional annotation defaults to rollback only when a RuntimeException is thrown.
Spring typically uses RuntimeException to represent unrecoverable error conditions. That is, for other exceptions, Spring doesn’t care and doesn’t roll back.
Here are two solutions:
@Override
@Transactional
public void thirdFunctionAboutException1(a){
try{
log.info("Destocking begins.");
goodsStockMapper.updateStock();
if(1= =1) throw new Exception();
}catch (Exception e){
log.info("Abnormal"+e.getMessage());
throw new RuntimeException("Manually throw RuntimeException"); }}@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2(a) throws Exception {
log.info("Destocking begins.");
goodsStockMapper.updateStock();
if(1= =1) throw new Exception();
}
Copy the code
The first is to manually throw a RuntimeException. The second is to change the Exception setting for the @Transactional rollback by default (RuntimeException inherits the Exception).
@Transactional(rollbackFor = Exception.class)
Here is a summary of the second pit avoidance guide:
By default, if we throw an exception that is not
RuntimeException
, the transaction still does not roll back; You need to manually throw itRuntimeException
Exception or change the @Transactional default configuration in Spring.
Fourth, the transaction is still not effective
Even if we are aware of the Transactional relationship between exceptions and @Transactional and avoid these pits correctly, we still fall into some pits that are much harder to spot and understand. In this section, we will continue with counterexamples, explain why transactions do not work in these cases, and suggest solutions. In this section you will also learn how @Transactional transactions relate to Spring AOP.
4.1 the sample a
Add two methods to the service:
@Override
public void privateFunctionCaller (a){
privateCallee();
}
@Transactional
private void privateCallee(a){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
Copy the code
The @Transactional annotated method privateCallee is indirectly called by calling the privateFunctionCaller method of the Service in the Controller.
After executing the code, the transaction is not rolled back. Why is that?
We annotate the @Service annotation on the Service class to indicate that the class is injected into the AOP container as a Bean, whereas Spring implements AOP through dynamic proxies. That is, beans in an AOP container are really proxy objects.
Spring supports @Transactional in this way by encapsulating methods in the original object (that is, when a method marked with this annotation is detected, it adds a transaction to it).
This behavior is called enhancing the target method. Although Spring implements dynamic proxies in CGLIB, I want to use the JDK’s dynamic proxy implementation here because it’s easier to understand.
As can be seen from service.function(), function must not be private if it goes proxy enhanced. So transactions on private methods do not take effect and cannot be rolled back.
In fact, when you write code like this, if you use the compiler IDEA, the compiler will raise an error, but only red, without any impact on compilation and execution.
Among the ways to implement dynamic proxies in Java are the JDK implementation and CGLIB. For those of you who don't understand dynamic proxies, learn about the proxy pattern and MaBtais implementation in Spring.
4.2 example 2
Can we just change private to public? The following code is a pit that many students often fall into when they first use @Transactional.
@Override
public void publicFunctionCaller (a){
publicCallee();
}
@Override
@Transactional
public void publicCallee(a){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
Copy the code
When we call the publicFunctionCaller of the Service in the Controller, we find that the transaction still cannot be rolled back. Why?
As we mentioned earlier, the injected Service object in Controller is its proxy object. There is no @Transactional annotation on it when the publicCallee method is called.
Function (), that is, the original object of the service calls its own publicFunctionCaller method first. It calls its own publicCallee method. PublicCallee methods enhanced by proxy objects (with transactions) are never used at all. Natural transactions do not roll back.
Solution, I think we will be able to find their own, that is the bean in the Controller by injecting service called directly indicate the @ Transactional methods, such as in the above secondFunctionAboutException is invoked.
Of course, we can also save the country by injecting ourselves into the service so that we can implement proxy objects to call enhanced methods:
@Override
@Transactional
public void publicCallee(a){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
@Autowired
private GoodsStockService self;
@Override
public void aopSelfCaller (a){
self.publicCallee();
}
Copy the code
But it’s clearly not hierarchical or elegant.
Here is a summary of the third pit avoidance guide:
Methods marked with @transactioal annotations must be public and must be called directly by the injected bean for transaction rollback.
That’s the end of the @Transactional guide to avoiding pits. Let’s make conversation in the comments if you have any questions.
I also hope you can praise a lot, pay attention to my public number, and will continue to output more high-quality articles in the future! Search: BeerBear
All the code in this article, put on Gitee, need to take small partners.