Why @Transactional Can Boost Query Performance in Spring Boot 3
This article examines the @Transactional annotation in Spring Boot, explains its proper and improper use, and presents extensive performance tests using JPA, JDBC, and EntityManager that reveal surprising cases where transactions improve query throughput.
1. Introduction
@Transactional is the core annotation for declarative transaction management in Spring, simplifying database transaction handling by separating transaction logic from business code. It automatically opens a transaction before method execution and commits or rolls back based on exceptions, improving readability and maintainability.
Misusing @Transactional can negatively affect system performance, such as overly large transaction scopes, unnecessary fine‑grained transactions, and using transactions in read‑only scenarios, leading to longer connection times, lock contention, and reduced throughput.
2. Practical Cases
2.1 Environment Setup
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/batch
username: root
password: 123123
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 10
---
spring:
jpa:
generateDdl: false
hibernate:
ddlAuto: update
openInView: true
show-sql: false2.2 Entity Definition
@Entity
@Table(name = "o_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private Integer age;
private String phone;
private String sex;
// getters, setters
}2.3 Repository Query Test (JPA)
Test 1 – without @Transactional:
private final UserRepository userRepository;
public User queryUser() {
return this.userRepository.findById(4888888).orElse(null);
}Average throughput: 9 700 ops/s.
Test 2 – with @Transactional:
@Transactional
public User queryUser() {}Average throughput: 12 700 ops/s, unexpectedly higher.
Test 3 – read‑only transaction:
@Transactional(readOnly = true)
public User queryUser() {}Average throughput: 9 300 ops/s, similar to no transaction.
2.4 JDBC Query Test
Test 1 – without @Transactional (using JdbcTemplate):
private final JdbcTemplate jdbcTemplate;
public User queryUser() {
return this.jdbcTemplate.queryForObject(
"select id, name, age, phone, sex from o_user where id = 4888888",
(rs, rowNum) -> {
User user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setPhone(rs.getString("phone"));
user.setSex(rs.getString("sex"));
return user;
});
}Average throughput: 25 000 ops/s.
Test 2 – with @Transactional:
@Transactional
public User queryUser() {}Average throughput: 13 100 ops/s, performance drops as expected.
Test 3 – read‑only transaction:
@Transactional(readOnly = true)
public User queryUser() {}Average throughput: 9 800 ops/s, comparable to the read‑write case.
2.5 EntityManager Query Test
Test 1 – without @Transactional:
private final EntityManager em;
public User queryUser() {
return this.em.find(User.class, 4888888);
}Average throughput: 24 000 ops/s.
Test 2 – with @Transactional:
@Transactional
public User queryUser() {
return this.em.find(User.class, 4888888);
}Average throughput: 13 000 ops/s.
Test 3 – read‑only transaction:
@Transactional(readOnly = true)
public User queryUser() {}Average throughput: 9 800 ops/s.
2.6 Summary of Transactional Queries
Consistency: a transaction provides a snapshot of data at a single point in time, avoiding inconsistencies caused by concurrent modifications.
Performance optimization: Spring’s @Transactional(readOnly = true) allows the database to apply read‑only optimizations such as snapshot reads and reduced lock contention.
Connection reuse: multiple operations within the same transaction can share a single database connection, lowering connection overhead.
Compatibility with write operations: if a read method later becomes part of a larger write transaction, the existing @Transactional annotation integrates seamlessly.
Example of a read‑only service method that fetches user and order data in one transaction:
private final UserRepository userRepository;
private final OrderRepository orderRepository;
@Transactional(readOnly = true)
public UserInfoDto getUserInfo(Long userId) {
User user = userRepository.findById(userId);
List<Order> orders = orderRepository.findByUserId(userId);
return new UserInfoDto(user, orders);
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
