JPA Projection 详解(接口投影 / 类投影 / 动态投影 / 原生SQL映射)
作者:职业帅哥
适用技术栈:Spring Data JPA / Hibernate
适用场景:大数据量查询优化、DTO 映射、接口隔离、微服务接口瘦身
一、什么是 Projection(投影)?
Projection 是指: > 只查询和返回实体的一部分字段,而不是整个 Entity对象。 这类似于SQL中的 SELECT column1, column2 而非 SELECT *。
🎯 为什么要使用 Projection?
优点 说明
减少 IO 避免加载无用字段
提升性能 降低 Hibernate 实体管理成本
避免懒加载陷阱 不触发关联加载
更适合接口返回 天然 DTO
安全 防止敏感字段泄露
二、Projection 分类总览
Spring Data JPA 支持多种投影方式:
| 类型 | 特点 | 推荐指数 |
|---|---|---|
| Interface Projection | 自动代理、零侵入 | ⭐⭐⭐⭐⭐ |
| Class Projection (DTO) | 构造器映射 | ⭐⭐⭐⭐ |
| Dynamic Projection | 运行时切换返回类型 | ⭐⭐⭐ |
| Native SQL Projection | 原生 SQL 映射 | ⭐⭐⭐ |
三、准备示例实体
java
@Entity
@Table(name = "user")
public class UserEntity {
@Id
private Long id;
private String username;
private String email;
private Integer age;
private String password;
}
四、接口投影(Interface Projection)⭐⭐⭐⭐⭐
4.1 定义投影接口
java
public interface UserSimpleView {
Long getId();
String getUsername();
Integer getAge();
}
✔ 方法名必须与 Entity 字段一致(getter 规则)
4.2 Repository 使用
java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
List<UserSimpleView> findByAgeGreaterThan(Integer age);
}
执行 SQL:
sql
select id, username, age from user where age > ?
4.3 嵌套投影
java
public interface OrderView {
Long getId();
UserView getUser();
interface UserView {
String getUsername();
}
}
4.4 SpEL 计算字段
java
public interface UserView {
String getUsername();
@Value("#{target.username + ' (' + target.age + ')'}")
String getDisplayName();
}
五、DTO 类投影(Class Projection)⭐⭐⭐⭐
5.1 DTO 定义
java
public class UserDTO {
private Long id;
private String username;
public UserDTO(Long id, String username) {
this.id = id;
this.username = username;
}
}
5.2 JPQL 构造器查询
java
@Query("select new com.demo.dto.UserDTO(u.id, u.username) from UserEntity u")
List<UserDTO> findUserDTOs();
⚠ 必须使用全类名。
5.3 优缺点
| 优点 | 缺点 |
|---|---|
| 类型安全 | 需要维护构造器 |
| 可复杂计算 | JPQL 可读性较差 |
| 可脱离实体 | 修改字段需同步修改 DTO |
六、动态投影(Dynamic Projection)⭐⭐⭐
6.1 Repository 定义
java
<T> List<T> findByAgeGreaterThan(Integer age, Class<T> type);
6.2 调用示例
java
List<UserSimpleView> views =
repo.findByAgeGreaterThan(18, UserSimpleView.class);
List<UserDTO> dtos =
repo.findByAgeGreaterThan(18, UserDTO.class);
6.3 使用场景
- 同一接口支持多个返回结构
- 后台管理 / 前台接口共用查询
七、原生 SQL Projection
7.1 接口映射
java
public interface UserNativeView {
Long getId();
String getUsername();
}
java
@Query(value = "select id as id, username as username from user", nativeQuery = true)
List<UserNativeView> findNativeUsers();
⚠ SQL 别名必须与接口方法名一致。
7.2 DTO 映射
java
@SqlResultSetMapping(...)
(略,复杂项目才建议使用)
八、分页 + Projection
java
Page<UserSimpleView> findByAgeGreaterThan(Integer age, Pageable pageable);
分页仍然有效。
九、排序 + Projection
java
findByAgeGreaterThan(Integer age, Sort sort);
十、常见坑总结
❌ 1. 字段名不一致
接口方法名必须与字段名匹配。
❌ 2. 使用 Lombok Getter 命名异常
确保生成 getXxx() 方法。
❌ 3. JSON 序列化失败
Projection 是代理对象,建议直接返回给前端无问题。
❌ 4. N+1 问题
嵌套投影仍可能触发延迟加载。
十一、性能建议
| 建议 | 原因 |
|---|---|
| 优先使用接口投影 | 最轻量 |
| 避免返回 Entity | 避免脏数据 |
| 只查必要字段 | 减少网络 IO |
| 大列表必须分页 | 防止 OOM |
| SQL 日志观察字段 | 验证投影是否生效 |
十二、与你现有项目的结合建议
结合你目前的:
- ✅ 微服务架构
- ✅ 大量统计查询
- ✅ 自定义参数转义模块
- ✅ 多表统计分析
推荐:
Controller 层返回 Projection 接口,而不是 Entity 或 VO。
示例:
java
public interface AlarmStatView {
String getProjectName();
Long getTotalCount();
}
Mapper:
java
List<AlarmStatView> statByProject(...);
直接用于图表接口,零 DTO 转换成本。