Pass the spring official certification and find out what's going on with transaction communication

Posted by dbomb101 on Sun, 23 Jan 2022 13:11:02 +0100

Pass the spring official certification and find out what's going on with transaction communication

Because of the epidemic situation in 2020, the company has been in a recession. Since October 2020, the company has only paid 80% of the salary. Since February this year, it has announced that it will only pay 60% of the salary temporarily, or choose to leave voluntarily. For the "payment on another day" that had not been paid before, it was hesitant at that time. It has accumulated 20% of the salary for four months, which is also a lot of income. If you leave, I don't know when it will be delivered, and the current environment is not very good, and the projects in hand can't be handed over for a moment and a half. I heard from the supervisor that there are tens of millions in the company's account, and I'm also talking about new outsourcing projects. There is a turning point, so I chose to receive 60% of the salary. As a result, the company directly declared bankruptcy before May. Fortunately, the boss is good, The social security has not been cut off. Finally, he made up the money for us. After calculation, almost all the unpaid wages have been made up.

Although I got my salary, I didn't have a job. The former supervisor of the company recommended me a job and I went for an interview the day before yesterday. Before the interview, I was still very confident. First, it was recommended by acquaintances. Second, I had more than two years of work experience. As a result, I didn't expect to be asked a very simple question, The topic is as follows: "there are two methods for updating data in a business class, which are transactional. If the second method calls the first method, how many transactions will there be?", If you convert text into code, it is roughly like this:

public class UserService {
  @Transactional
  public void update1() {
    // Perform some actions
  }
  
  @Transactional
  public void update2() {
    update1(); // Call another transactional method in the current class
  }
}

This obviously examines the spread of affairs! To be honest, although I have more than 2 years of working experience, the company is not large, and there are no too complex transactions in the projects I participate in. Generally, it is completed by adding @ Transactional annotation to the business methods that need Transactional. As long as the test results are OK, I have never been tangled with the propagation type of transactions, Fortunately, I turned over the notes given by the teacher during the training the night before the interview and answered them smoothly. There are several types of communication of affairs:

  • MANDATORY
  • NEVER
  • NOT_SUPPORTED
  • SUPPORTS
  • REQUIRED (default)
  • REQUIRES_NEW
  • NESTED

Therefore, the answer to this interview question is: "two Transactional methods, one calls the other. Since the default value of transaction propagation is REQUIRED, it is expressed as: create if there is no transaction at present, and use if there is a transaction at present". In order to improve my answer, I also continued to add: "if the propagation property of the @ Transactional annotation is configured to other values, it will be different". When I finished the answer in my head very fluently, the interviewer smiled and said two words: "no", I was stunned at that time. Finally, the interviewer didn't tell me the answer, just let me go back to find the answer... But I was lucky. Because I only made a mistake in this question, I finally entered the job smoothly.

I think every code farmer has a certain dedication to technology. After the interview the day before yesterday, he also read some articles on the Internet. Most of them only talked about the communication types of affairs and various types of performance, and there was no answer I wanted. So yesterday, I contacted Mr. Cang during the training. After listening to the questions and my answers, he also smiled "ha ha", It was said that this was the original question in the Spring certification test, and the probability of being admitted to this question was at least 70%. Moreover, recently, many companies directly took the questions in the Spring question bank as interview questions... Then he asked me to wait, and later sent me a compressed package, which was a Demo code. As expected, there were few cruel words. I took the code directly to reason. I looked at the code, According to the comments left by Mr. Cang in the code, I changed it a few times, and basically there is an answer!

Although the answer itself is very simple, I have learned a lot. In order to "commemorate" this wrong question, I would like to share with you the details of @ Transactional in Spring!

First, the project structure is as follows:

[the external link image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-pbGIxnx9-1625792886357)(image-20210630170414935.png)]

Spring, Mybatis and unit testing are mainly used in this project. I won't mention the basic environment construction and configuration. If you need this code, you can download it from http: / / www.

Probably: t is used in the project_ User and t_ The two tables of order have several initial data. The persistence layer corresponding to these two tables has the function of modifying data according to id.

The focus is on the business part. As we all know, transactions are managed at the business layer. The structure of the business layer is as follows (if not used for the time being, don't post it first):

[src]
		[main]
				[java]
						[cn.tedu]
								[service]
										[impl]
												UserServiceImpl
										UserService

Obviously, everything that starts with the User prefix is processing t_ In the initial experiment, you only need to observe this table.

About UserService interface:

package cn.tedu.service;

public interface UserService {

    void update1();

    void update2();

}

About UserServiceImpl class:

package cn.tedu.service.impl;

import cn.tedu.mapper.UserMapper;
import cn.tedu.service.OrderService;
import cn.tedu.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    private UserMapper userMapper;
    private OrderService orderService;

    public UserServiceImpl(UserMapper userMapper, OrderService orderService) {
        this.userMapper = userMapper;
        this.orderService = orderService;
    }

    // TODO-01: adjust whether to use the following @ Transactional annotation and run unit tests to observe the effect
  	// @Transactional
    public void update1() {
        int rows;
        // Updating the data with id=1 will succeed
        rows = userMapper.updateUserNameById(1, "USER-1000001");
        if (rows != 1) {
            throw new RuntimeException("to update User:id=1 Data failure!");
        }
        // Updating id=1000000 will fail
        rows = userMapper.updateUserNameById(1000000, "USER-1000001");
        if (rows != 1) {
            throw new RuntimeException("to update User:id=1000000 Data failure!");
        }
    }

    // TODO-02: adjust whether to use the following @ Transactional annotation and run unit tests to observe the effect
    @Transactional
    public void update2() {
        update1();
    }

}

It can be seen that there are two update operations in the above update1() method. The first one will certainly succeed, and the second one will fail because the id value does not exist. After the failure, a RuntimeException object is thrown, which is in line with the default rollback rules of Spring management transactions. However, the update1() method does not necessarily have @ Transactional annotation, which is reserved by Mr. Cang for me to test the effect, The following update2() is relatively simple. It directly calls the Update1 () method.

The test written by Mr. Cang is also very interesting. The @ Sql annotation is used to initialize the database and data, and the assertion is used. It is completely different from what we usually write lazily. I asked him that day, and he said that the Spring certification test will also take this test, In the future, it may also become an interview question for employers (after all, many employers directly go online to Baidu to find interview questions, and they don't make their own mistakes. Everyone knows)... He wrote this:

package cn.tedu.service;

import cn.tedu.config.ApplicationConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringJUnitConfig(ApplicationConfig.class)
@Sql(config = @SqlConfig(dataSource = "dataSource"),
        scripts = {"classpath:/sql/schema.sql", "classpath:/sql/data.sql"})
public class UserServiceTests {

    @Autowired
    UserService userService;

    @Test
    public void testUpdate1() {
        assertThrows(RuntimeException.class, () -> {
            userService.update1();
        });
    }

    @Test
    public void testUpdate2() {
        assertThrows(RuntimeException.class, () -> {
            userService.update2();
        });
    }

}

In fact, we can test the effect now. We can judge whether to use @ Transactional annotation on the two methods in the business class and observe whether the data is rolled back. The test results are as follows:

Whether to use annotation on update1()Whether to use annotations on update2()Rollback
yesyesyes
noyesyes
yesnono
nonono

It can be seen that whether the transaction is rolled back depends entirely on whether the update2() method has @ Transactional annotation, and has nothing to do with whether the update1() method has annotation!

Mr. Cang said that the official document given by Spring clearly states that "Propagation Rules Are Enforced by a Proxy", that is, "propagation rules are enforced by an agent". Therefore, Spring manages transactions based on the interface. Before calling the @ Transactional annotation method, the transaction will be opened and whether to roll back or commit will be decided in the process!

In the above code, since update1() is called inside update2() and not by proxy object, the process of executing update2() method is roughly as follows:

Open transaction
		implement update2()method
				call update1()method
    because update1()Method throws an exception and complies with the rollback rules to execute the rollback transaction
 If no rollback occurs, the transaction is committed (this example will roll back and this step will not be executed)

Therefore, back to the question I interviewed, the correct answer should be: only one transaction will be opened when calling the update2() method. The internally called update1() is not Transactional at all (with or without @ Transactional annotation). Since there is only one transaction, there is no transaction propagation!

In fact, my problem has been solved here, but Mr. Cang also helped me write the follow-up Demo code to make me more deeply understand that this may be the teacher's occupational disease. Either don't talk about it, just talk to the end.

The next step is to update t_ The data in the order table. The corresponding business interface and business implementation classes are OrderService and OrderServiceImpl respectively. About the OrderService interface:

package cn.tedu.service;

public interface OrderService {

    void updateSuccessfully();

}

About the OrderServiceImpl class:

package cn.tedu.service.impl;

import cn.tedu.mapper.OrderMapper;
import cn.tedu.service.OrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private OrderMapper orderMapper;

    public OrderServiceImpl(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    // TODO-08: run the unit test directly to observe the effect
    // TODO-09: enable the following @ Transactional annotation and run unit tests to observe the effect
    // TODO-10: enable the following annotated parameters and run unit tests to observe the effect
    // @Transactional //(propagation = Propagation.REQUIRES_NEW)
    public void updateSuccessfully() {
        // Updating the data with id=1 will succeed
        int rows;
        rows = orderMapper.updateNumberById(1, 1000000);
        if (rows != 1) {
            throw new RuntimeException("to update Order:id=1 Data failure!");
        }
    }

}

Obviously, the above business is very simple, that is, to successfully update a piece of data, write notes and parameters in advance before the method, and adjust them later to observe the effect.

In addition, in the update2() method of UserServiceImpl class, after adjustment according to the comments left by the teacher, the valid code is:

@Transactional
public void update2() {
  	// Updating the data with id=2 will succeed
    int rows;
    rows = userMapper.updateUserNameById(2, "USER-1000002");
    if (rows != 1) {
       throw new RuntimeException("to update id=2 Data failure!");
    }

  	// Call the update method of another business object
    orderService.updateSuccessfully();

  	// Updating data with id=2000000 will fail
    rows = userMapper.updateUserNameById(2000000, "USER-1000002");
    if (rows != 1) {
       throw new RuntimeException("to update id=2000000 Data failure!");
    }
}

Therefore, call the above update2() method at this time, and the process will be:

to update id=2 The data will be successful
 Calling the update method of another business object will succeed
 to update id=2000000 The data will fail

After several tests, the results are as follows:

Annotation status on updateSuccessfully() method of OrderServiceImplRollback status
No commentFull rollback
@TransactionalFull rollback
@Transactional(propagation = Propagation.REQUIRES_NEW)t_user table rollback, t_order table submitted

You can see that when @ transactional (propagation = propagation. Requirements_new) is used for the last time, the updateSuccessfully() method in OrderServiceImpl runs on a new transaction (determined by the configured annotation parameters). Since there is no error in the operation of this updateSuccessfully() method, it is submitted directly, In UserServiceImpl, update2() failed to update the data with id=2000000, resulting in rollback, so t_ The user table rolled back and t_ The phenomenon that the order table is submitted reflects the propagation of transactions!

What's the reason for calling another transactional method in update2()? This is because the method of another object is called this time, and this object is also a proxy object generated by Spring's transaction management mechanism. Its execution process is roughly as follows:

Open transaction
		implement update2()Method( UserServiceImpl Class)
				to update id=2 Data, and successful
				
				Open new transaction
						call updateSuccessfully()Method( OrderServiceImpl Class)
				If no rollback occurs, the transaction is committed
				
				to update id=2000000 And failed, execute rollback transaction
 If no rollback occurs, the transaction is committed (this example will roll back, and this step will not be executed)

Finally, Mr. Cang left me a todo final. The code is very simple. I output the class name of orderService in the update2() method:

// Todo final: enable the next line of code and run unit tests to see the effect
System.out.println(orderService.getClass());

Then, you can clearly see the proxy object on the console:

class com.sun.proxy.$Proxy37

Note: you cannot only output orderService, it must be orderService getClass(), because the proxy object overrides the toString() method. If getClass() is not called, you will see CN tedu. service. impl. OrderServiceImpl@5f7b97da , you can't see that it's a proxy object. Is this guy very cunning?

These points can be summarized through this Demo:

  • Spring manages transactions based on interface proxy;
  • For calls between methods of the current class, there is no transaction propagation. Whether to add @ Transactional annotation before the called method has no impact on the result;
  • For calls between methods of different classes, for the called methods, the transaction propagation type can be configured through the propagation attribute of the @ Transactional annotation.

In addition, there is an additional gain. In the past, every time I wrote business layer code, I wrote business interfaces first and then implementation classes. Why do I have a business interface? I haven't delved into this problem for a long time. I just regard it as a development specification to comply with. Now it seems that the significance is not only here!

When executing the unit test, I also deliberately tried. If the business object is declared as UserServiceImpl, the automatic assembly will be prompted during the startup process. The project cannot run at all. It must be declared as UserService. In addition, if it is declared as UserServiceImpl, as long as there is no @ Transactional annotation in the whole process, Starting the project will not report an error. As for the truth, I believe you have guessed it, so I won't explain it.

Topics: Spring