Projection + record
Java Record
Java Record(Java 16 引入,Java 17+ 正式版)是一种特殊的类,专门用于不可变数据载体;它自动生成 构造器、getter、equals()、hashCode()、toString(),极大减少了样板代码:
// 传统 POJO —— 需要大量样板代码
public class UserDTO {
private final Long id;
private final String username;
private final String email;
// 构造器、getter、equals、hashCode、toString...(50+ 行)
}
// Record —— 一行搞定
public record UserDTO(Long id, String username, String email) {}
Projection(投影) 的核心思想是:只查询和返回需要的字段,而不是加载整个实体的所有列。
- 在 Spring Data JPA 中,Projection 是框架内置的概念(接口投影、类投影)
- 在 MyBatis 中,Projection 是通过 SQL 的
SELECT子句 + 自定义结果映射类型来实现的;( Spring Data JPA 和 MyBatis )
Projection + Record:是将两者结合,用 Java Record 作为投影的目标类型,只查询需要的字段,映射到一个不可变的 Record 中。
Spring Data JPA 和 MyBatis 是两种主流的持久层框架,两者的设计理念和工作方式截然不同;因为 MyBatis 面向 SQL,极其灵活,能够应对各种复杂场景,因此使用更加广泛。
由于 Record 没有 Setter 方法,MyBatis 会通过全参构造函数进行实例化;
在 DTO 和 Projection 场景中,应该全面废弃 Lombok 的 @Data;
2026年的最佳实践是结合 JSpecify 来定义 Record,如下:
public record UserProjection(
Long id,
String username,
@Nullable String nickname // 明确标记可能为空的字段
) {}
在 Spring Boot 4.0.5 + MyBatis 中的使用
方式一:注解方式(推荐用于简单查询)
java
// 1. 定义 Projection Record
public record UserSummary(Long id, String username, String email) {}
// 2. Mapper 接口
@Mapper
public interface UserMapper {
// 简单情况:列名与 Record 组件名完全一致,MyBatis 可自动通过构造器映射
@Select("SELECT id, username, email FROM users WHERE active = true")
List<UserSummary> findActiveUserSummaries();
// 复杂情况:列名不一致时,使用 @ConstructorArgs 显式映射
@Select("SELECT u.id, u.user_name, u.email_addr FROM users u WHERE u.id = #{id}")
@ConstructorArgs({
@Arg(column = "id", javaType = Long.class),
@Arg(column = "user_name", javaType = String.class),
@Arg(column = "email_addr", javaType = String.class)
})
UserSummary findUserById(@Param("id") Long id);
}
方式二:XML 方式(推荐用于复杂查询)
xml
<!-- UserMapper.xml -->
<resultMap id="userSummaryMap" type="com.example.dto.UserSummary">
<constructor>
<arg column="id" javaType="java.lang.Long"/>
<arg column="user_name" javaType="java.lang.String"/>
<arg column="email_addr" javaType="java.lang.String"/>
</constructor>
</resultMap>
<select id="findActiveUsers" resultMap="userSummaryMap">
SELECT id, user_name, email_addr
FROM users
WHERE active = 1
</select>
方式三:自动映射(MyBatis 3.5.10+,最简洁)
当 SQL 列名/别名与 Record 组件名完全匹配时,MyBatis 可以自动通过构造器注入,无需额外配置:
java
public record UserSummary(Long id, String username, String email) {}
@Mapper
public interface UserMapper {
// 列名与 Record 组件名一致,自动映射
@Select("SELECT id, username, email FROM users WHERE id = #{id}")
UserSummary findById(@Param("id") Long id);
}
2026 年最佳实践(Spring Boot 4.0.5 + MyBatis)
1>. 分层架构中的 Record 使用
┌─────────────────────────────────────────────────────────┐ │ Controller 层 ← 使用 Response Record(API 响应) │ │ Service 层 ← 业务逻辑、Record 之间的转换 │ │ Mapper 层 ← 使用 Projection Record(数据库投影) │ │ 数据库 ← 只查询需要的列 │ └─────────────────────────────────────────────────────────┘
// ✅ Mapper 层:投影 Record(只包含数据库字段)
public record UserProjection(Long id, String username, String email, LocalDateTime createdAt) {}
// ✅ Controller 层:响应 Record(面向前端,可与投影不同)
public record UserResponse(Long id, String username, String email, String createdAtFormatted) {}
// ✅ Service 层:转换
@Service
public class UserService {
private final UserMapper userMapper;
public UserResponse getUserById(Long id) {
UserProjection projection = userMapper.findById(id);
return new UserResponse(
projection.id(),
projection.username(),
projection.email(),
projection.createdAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
);
}
}
2. Record 命名规范


3. 复杂查询的处理策略
// ✅ 对于一对多关系,使用嵌套查询 + 在 Service 层组装
public record OrderProjection(Long orderId, String orderNo, BigDecimal totalAmount) {}
public record OrderItemProjection(Long orderId, String productName, Integer quantity) {}
public record OrderDetailResponse(
Long orderId,
String orderNo,
BigDecimal totalAmount,
List<OrderItemProjection> items // 在 Service 层组装
) {}
@Service
public class OrderService {
public OrderDetailResponse getOrderDetail(Long orderId) {
OrderProjection order = orderMapper.findOrderById(orderId);
List<OrderItemProjection> items = orderMapper.findItemsByOrderId(orderId);
return new OrderDetailResponse(
order.orderId(), order.orderNo(), order.totalAmount(), items
);
}
}
4. 与 Spring Boot 4.0.5 新特性结合
// ✅ 用 Record 做不可变配置(Spring Boot 4.0.5 完全支持)
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(String host, int port, String from) {}
// ✅ 用 Record 做 Controller 请求体绑定
@RestController
@RequestMapping("/api/users")
public class UserController {
public record CreateUserRequest(
@NotBlank String username,
@Email String email,
@Size(min = 6) String password
) {} // Record + Jakarta Validation 注解
@PostMapping
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
}
5. 避免的反模式
// ❌ 不要给 Record 添加业务逻辑
public record UserProjection(Long id, String username) {
public boolean isAdmin() { return "admin".equals(username); } // 不推荐
}
// ❌ 不要用 Record 做可变实体(Record 天然不可变)
// 如果需要更新数据库,使用 Map 或专门的 Command/Request 对象
// ❌ 不要用一个 Record 满足所有场景
// 应该为不同的查询场景创建不同的 Projection Record
// ✅ 正确做法:按场景拆分
public record UserListItem(Long id, String username) {} // 列表页
public record UserDetail(Long id, String username, String email, String phone) {} // 详情页
public record UserBrief(String username) {} // 下拉选择器
核心原则:所有只读数据传输对象都应该优先使用 Record。只有在需要可变对象时,才使用传统 POJO。
通常 1 个 Entity 对应 1~3 个 Projection,因此 Record 不会太多;如果 Record 超过 5 个了,说明可能过度设计了。
可变用 Record,不可变用 Entity; 两者职责不同,不是替代关系。
Record 不可变 和它能不能用于写操作是两个不同的概念;只要写入时不需要改变实体,那 Record 完全可以使用,也推荐使用;如下:
// 前端传来 JSON → Spring 反序列化成 Record → 一次性构造完毕,之后不再修改
public record UserCreateRequest(@NotBlank String username, @Email String email) {}
Record 本身是不可变的(没有 setter),它的用途是承载写操作的入参。流程是:

即,entity 是"要改的",record 是"不用改的";Request 虽然服务于写流程,但它自己不需要被改,所以应该使用 record。
Entity(实体):代表数据库中的一条记录,是与表结构 1:1 映射的基石。
Record(投影/DTO):代表某个业务场景下的数据视图,是不可变的快照。
安全性:如果把 User Entity 返回给前端,可能会无意中泄露 password_hash 或 internal_remark 等敏感字段。
性能:Entity 往往包含很多大字段(如 long_text 类型的备注),但在列表展示时只需要 id 和 name;用 Record 做投影可以显著减少内存占用和 SQL 查询开销。
内部 Record(Scoped Records):如果一个 Record 只用于某个特定的 Service 方法,可以将其定义在接口内部或类内部,避免污染全局命名空间。
按需聚合:不要为每个微小的差异都建 Record。如果两个接口需要的字段 90% 重合,可以复用。
管理 50 个清晰的 Record,远比维护一个带有 100 个字段、充满 if(null) 判断、且不知道哪些字段在哪个接口被使用的“超级实体”要容易得多。