一、准备
a.实体映射(Entity Mapping)
Order实体:
java
import java.math.BigDecimal;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Table;
import javax.persistence.Id;
import lombok.Data;
@Entity
@Table(name = "t_demo_order")
@Data
public class DemoOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal amount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private DemoUser demoUser;
}
用户实体:
java
import java.util.ArrayList;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.OneToMany;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import java.util.List;
@Entity
@Table(name = "t_demo_user")
@Data
public class DemoUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@OneToMany(
mappedBy = "demoUser",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private List<DemoOrder> orders = new ArrayList<>();
public void addOrder(DemoOrder order) {
orders.add(order);
order.setDemoUser(this);
}
}
b.Repository 层(核心)
java
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.sky.dto.DemoUserOrderCountDTO;
import com.sky.entity.DemoUser;
@Repository
public interface DemoUserRepository extends JpaRepository<DemoUser, Long> {
// ❌ 默认不会加载 orders
List<DemoUser> findAll();
@Query("SELECT u FROM DemoUser u JOIN FETCH u.orders")
List<DemoUser> findAllWithOrders();
@Query("SELECT u.id AS id, " +
"u.username AS username, " +
"COUNT(o) AS orderCount " +
"FROM DemoUser u " +
"LEFT JOIN u.orders o " +
"GROUP BY u.id, u.username")
List<DemoUserOrderCountDTO> findUserOrderCount();
}
c. 事务管理(Transaction)
java
import java.util.List;
import com.sky.dto.DemoUserDTO;
public interface DemoUserService {
List<DemoUserDTO> listUsersBad();
List<DemoUserDTO> listUsersFetch();
List<DemoUserDTO> listUsersDto();
}
服务实现
java
import java.util.List;
import java.util.stream.Collectors;
import com.sky.dto.DemoUserDTO;
import com.sky.entity.DemoUser;
import com.sky.repository.DemoUserRepository;
import com.sky.service.DemoUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class DemoUserServiceImpl implements DemoUserService {
@Autowired
private final DemoUserRepository userRepository;
@Override
// ❌ N+1 问题示例
@Transactional(readOnly = true)
public List<DemoUserDTO> listUsersBad() {
// TODO Auto-generated method stub
// throw new UnsupportedOperationException("Unimplemented method
// 'listUsersBad'");
List<DemoUser> users = userRepository.findAll();
return users.stream()
.map(u -> new DemoUserDTO(
u.getId(),
u.getUsername(),
u.getOrders().size() // ❌ 触发 N+1
))
.collect(Collectors.toList());
}
@Override
// ✅ JOIN FETCH 解决 N+1
@Transactional(readOnly = true)
public List<DemoUserDTO> listUsersFetch() {
// TODO Auto-generated method stub
List<DemoUser> users = userRepository.findAllWithOrders();
return users.stream()
.map(u -> new DemoUserDTO(
u.getId(),
u.getUsername(),
u.getOrders().size()))
.collect(Collectors.toList());
}
@Override
// ✅ JOIN FETCH 解决 N+1
@Transactional(readOnly = true)
public List<DemoUserDTO> listUsersDto() {
// TODO Auto-generated method stub
return userRepository.findUserOrderCount()
.stream()
.map(d -> new DemoUserDTO(
d.getId(),
d.getUsername(),
d.getOrderCount().intValue()))
.collect(Collectors.toList());
}
}
d.DTO
java
public interface UserOrderCountDTO {
Long getId();
String getUsername();
Long getOrderCount();
}
控制器
java
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.sky.dto.DemoUserDTO;
import com.sky.service.DemoUserService;
import lombok.RequiredArgsConstructor;
//N+1 的本质是 Lazy 加载 + 循环访问关联对象
//生产环境最优解是 DTO 投影,其次是 JOIN FETCH
@RestController
@RequestMapping("/demo/users")
@RequiredArgsConstructor
public class DemoUserController {
private final DemoUserService userService;
@GetMapping("/bad")
public List<DemoUserDTO> bad() {
return userService.listUsersBad();
}
@GetMapping("/fetch")
public List<DemoUserDTO> fetch() {
return userService.listUsersFetch();
}
@GetMapping("/dto")
public List<DemoUserDTO> dto() {
return userService.listUsersDto();
}
}
SQL数据
sql
delete from t_demo_order;
delete from t_demo_user;
INSERT INTO t_demo_user (username) VALUES
('demo_user_01'),('demo_user_02');
-- 订单
INSERT INTO t_demo_order (amount, user_id) VALUES
-- user 1 有 3 单
(100.00, 1),
(110.00, 1),
(120.00, 1),
-- user 2 有 2 单
(200.00, 2),
(220.00, 2)
;
测试
使用http
1.N+1请求发送
GET http://localhost:9001/demo/users/bad
Accept: application/json
截图:

解决方案
a.fetch
java
@Query("SELECT u FROM DemoUser u JOIN FETCH u.orders")
List<DemoUser> findAllWithOrders();
测试:
GET http://localhost:9001/demo/users/fetch
Accept: application/json
截图:

b.EntityGraph(推荐,语义更清晰)
定义实体的修改:
java
@NamedEntityGraph(
name = "User.withOrders",
attributeNodes = @NamedAttributeNode("orders")
)
public class DemoUser
对Repository 接口
java
@EntityGraph(value = "User.withOrders", type = EntityGraph.EntityGraphType.FETCH) //// 不写 JPQL
List<DemoUser> findAll();
c.DTO
接口
java
public interface DemoUserOrderCountDTO {
Long getId();
String getUsername();
Long getOrderCount();
}
实现方法:Repository
java
@Query("SELECT u.id AS id, " +
"u.username AS username, " +
"COUNT(o) AS orderCount " +
"FROM DemoUser u " +
"LEFT JOIN u.orders o " +
"GROUP BY u.id, u.username")
List<DemoUserOrderCountDTO> findUserOrderCount();
d.Batch Fetching(补救方案)
老项目补救
java
spring:
jpa:
properties:
hibernate:
batch_fetch_style: IN
default_batch_fetch_size: 50
sql 效果:
sql
select * from t_order where user_id in (?, ?, ?, ...)
总结:
场景 推荐方案
简单列表 DTO 投影
需要 Entity JOIN FETCH / EntityGraph
老项目补救 Batch Fetch
分页 + 关联 DTO + 子查询
用 Hibernate Statistics 验证 SQL 数量
1.开启 Hibernate Statistics
bash
spring:
jpa:
properties:
hibernate:
generate_statistics: true
2.注入 SessionFactory
bash
@Autowired
private EntityManagerFactory entityManagerFactory;
3.写测试方法(核心)
bash
@Transactional(readOnly = true)
public void testNPlusOne() {
SessionFactory sf = entityManagerFactory.unwrap(SessionFactory.class);
Statistics stats = sf.getStatistics();
stats.clear();
List<DemoUser> users = userRepository.findAll();
users.forEach(u -> u.getOrders().size());
System.out.println("SQL count = " + stats.getPrepareStatementCount());
}
测试引用
bash
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
java
@DataJpaTest
@Import(DemoDataInitializer.class) // 插入测试数据
class DemoUserRepositoryTest {
@Autowired
private DemoUserRepository userRepository;
@Autowired
private EntityManagerFactory emf;
@Test
void should_not_cause_n_plus_one() {
SessionFactory sf = emf.unwrap(SessionFactory.class);
Statistics stats = sf.getStatistics();
stats.clear();
List<DemoUser> users = userRepository.findAllWithOrders();
users.forEach(u -> u.getOrders().size());
assertThat(stats.getPrepareStatementCount()).isEqualTo(1);
}
}
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoUserControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void should_return_users_without_n_plus_one() {
var result = restTemplate.exchange(
"/demo/users/dto",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<DemoUserDTO>>() {}
);
assertThat(result.getBody()).hasSize(10);
}
}
分页错乱
错误代码如下:
java
@Query("SELECT u FROM DemoUser u JOIN FETCH u.orders")
Page<DemoUser> findAllWithOrders(Pageable pageable);
java
@Transactional(readOnly = true)
public Page<DemoUserDTO> listUsers(Pageable pageable) {
Page<DemoUser> page = userRepository.findAllWithOrders(pageable);
return page.map(u -> new DemoUserDTO(
u.getId(),
u.getUsername(),
u.getOrders().size()
));
}
正确代码如下:
先查 ID,再 FETCH
java
@Query("SELECT u.id FROM DemoUser u")
Page<Long> findUserIdPage(Pageable pageable);
@Query("SELECT u FROM DemoUser u JOIN FETCH u.orders WHERE u.id IN :ids")
List<DemoUser> findAllWithOrdersByIds(@Param("ids") List<Long> ids);
java
@Transactional(readOnly = true)
public Page<DemoUserDTO> listUsers(Pageable pageable) {
Page<Long> idPage = userRepository.findUserIdPage(pageable);
List<DemoUser> users =
userRepository.findAllWithOrdersByIds(idPage.getContent());
Map<Long, DemoUser> userMap = users.stream()
.collect(Collectors.toMap(DemoUser::getId, Function.identity()));
List<DemoUserDTO> dtos = idPage.getContent().stream()
.map(id -> {
DemoUser u = userMap.get(id);
return new DemoUserDTO(
u.getId(),
u.getUsername(),
u.getOrders().size()
);
})
.toList();
return new PageImpl<>(dtos, pageable, idPage.getTotalElements());
}
DTO 投影(无分页问题)
总结:
JOIN FETCH 会放大结果集行数,而 Spring Data 的分页是基于行数的,因此会导致分页错乱。
正确做法是:ID 分页 + 批量 FETCH,或直接用 DTO 投影。