1. 简介
在 Java 持久化 API(JPA)中,SqlResultSetMapping
是一个相对冷门但非常实用的注解,尤其在处理原生 SQL 查询时能发挥重要作用。
它的核心功能是:将数据库原生 SQL 查询返回的结果集,映射到 Java 对象中——无论是基本类型、自定义对象,还是多个实体的组合结果。✅
相比 JPQL 或 Spring Data JPA 的方法名推导,原生 SQL 更灵活,但也更“原始”。而 SqlResultSetMapping
正是用来“驯服”这种原始结果集的关键工具。
2. 环境准备
要演示 SqlResultSetMapping
的用法,我们需要先搭建基础环境。
2.1. Maven 依赖
项目中需要引入 JPA 实现(Hibernate)和一个嵌入式数据库,这里使用 H2:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.15.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
Hibernate 提供 JPA 规范的实现,H2 则用于内存数据库测试,方便快速验证。
2.2. 数据库表结构
我们创建两张表:EMPLOYEE
和 SCHEDULE_DAYS
,模拟员工及其排班信息。
CREATE TABLE EMPLOYEE (
id BIGINT,
name VARCHAR(10)
);
CREATE TABLE SCHEDULE_DAYS (
id IDENTITY,
employeeId BIGINT,
dayOfWeek VARCHAR(10)
);
其中 SCHEDULE_DAYS
通过 employeeId
关联到 EMPLOYEE
。测试数据可通过 GitHub 示例项目生成(见文末源码)。
2.3. 实体类定义
对应的 JPA 实体类如下:
@Entity
public class Employee {
@Id
private Long id;
private String name;
}
@Entity
@Table(name = "SCHEDULE_DAYS")
public class ScheduledDay {
@Id
@GeneratedValue
private Long id;
private Long employeeId;
private String dayOfWeek;
}
⚠️ 注意:ScheduledDay
类名与表名不一致,因此使用 @Table
显式指定映射关系。
3. 标量结果映射(Scalar Mapping)
当你只需要从 SQL 查询中提取某些字段(如 ID、计数等),并映射为基本类型时,ColumnResult
是最直接的选择。
3.1. 使用 @ColumnResult
SqlResultSetMapping
必须指定 name
,这是它与其他注解关联的“钥匙”。
下面定义一个映射,只取周五排班的员工 ID:
@SqlResultSetMapping(
name = "FridayEmployeeResult",
columns = {@ColumnResult(name = "employeeId")}
)
接着,在实体类上定义一个命名原生查询,引用该映射:
@NamedNativeQuery(
name = "FridayEmployees",
query = "SELECT employeeId FROM schedule_days WHERE dayOfWeek = 'FRIDAY'",
resultSetMapping = "FridayEmployeeResult"
)
✅ 关键点:resultSetMapping
的值必须与 SqlResultSetMapping
的 name
完全一致,否则会抛异常。
3.2. 测试标量映射
通过 EntityManager
调用命名查询,结果将自动映射为 Long
类型的列表:
@BeforeAll
public static void setup() {
emFactory = Persistence.createEntityManagerFactory("java-jpa-scheduled-day");
em = emFactory.createEntityManager();
}
@Test
public void whenNamedQuery_thenColumnResult() {
List<Long> employeeIds = em.createNamedQuery("FridayEmployees").getResultList();
assertEquals(2, employeeIds.size());
}
✅ 成功拿到两个员工 ID,说明映射生效。
4. 构造函数映射(Constructor Mapping)
当查询结果需要封装成完整对象时,ConstructorResult
就派上用场了。它通过调用目标类的构造函数来实例化对象。
4.1. 使用 @ConstructorResult
首先,为 ScheduledDay
添加一个自定义构造函数:
public ScheduledDay(Long id, Long employeeId, Integer hourIn, Integer hourOut, String dayofWeek) {
this.id = id;
this.employeeId = employeeId;
this.dayOfWeek = dayofWeek;
}
然后定义 SqlResultSetMapping
,指定构造函数参数对应的列:
@SqlResultSetMapping(
name = "ScheduleResult",
classes = {
@ConstructorResult(
targetClass = com.baeldung.sqlresultsetmapping.ScheduledDay.class,
columns = {
@ColumnResult(name = "id", type = Long.class),
@ColumnResult(name = "employeeId", type = Long.class),
@ColumnResult(name = "dayOfWeek")
}
)
}
)
⚠️ 列顺序必须与构造函数参数顺序严格一致,否则会因找不到匹配构造函数而失败。
关联查询如下:
@NamedNativeQuery(
name = "Schedules",
query = "SELECT * FROM schedule_days WHERE employeeId = 8",
resultSetMapping = "ScheduleResult"
)
💡 小贴士:如果 SQL 返回的字段类型与 Java 不一致(如 BIGINT → Long),建议显式指定 type
,避免类型转换踩坑。
4.2. 测试构造函数映射
@Test
public void whenNamedQuery_thenConstructorResult() {
List<ScheduledDay> scheduleDays = Collections.checkedList(
em.createNamedQuery("Schedules", ScheduledDay.class).getResultList(),
ScheduledDay.class
);
assertEquals(3, scheduleDays.size());
assertTrue(scheduleDays.stream().allMatch(c -> c.getEmployeeId().longValue() == 8));
}
✅ 映射成功,且对象处于“新建”或“游离”状态(detached),不受 EntityManager
管理。
5. 实体映射(Entity Mapping)
如果你希望将结果映射回标准 JPA 实体,EntityResult
是最合适的选项,尤其适合多表关联查询。
5.1. 单个实体映射
假设表中字段名与实体属性不一致(如 id
对应 employeeNumber
),可用 FieldResult
显式映射:
@SqlResultSetMapping(
name = "EmployeeResult",
entities = {
@EntityResult(
entityClass = com.baeldung.sqlresultsetmapping.Employee.class,
fields = {
@FieldResult(name = "id", column = "employeeNumber"),
@FieldResult(name = "name", column = "name")
}
)
}
)
对应查询需使用别名:
@NamedNativeQuery(
name = "Employees",
query = "SELECT id as employeeNumber, name FROM EMPLOYEE",
resultSetMapping = "EmployeeResult"
)
✅ EntityResult
要求目标类有默认构造函数(无参),这是 JPA 的基本要求。
5.2. 多实体映射
处理多表联查时,可同时映射多个实体:
@SqlResultSetMapping(
name = "EmployeeScheduleResults",
entities = {
@EntityResult(entityClass = com.baeldung.sqlresultsetmapping.Employee.class),
@EntityResult(entityClass = com.baeldung.sqlresultsetmapping.ScheduledDay.class)
}
)
5.3. 测试实体映射
单实体测试:
@Test
public void whenNamedQuery_thenSingleEntityResult() {
List<Employee> employees = Collections.checkedList(
em.createNamedQuery("Employees").getResultList(),
Employee.class
);
assertEquals(3, employees.size());
assertTrue(employees.stream().allMatch(e -> e.getClass() == Employee.class));
}
多实体测试:
由于多实体映射涉及联表,查询无法直接绑定到单一实体类上,因此在测试中手动创建原生查询:
@Test
public void whenNamedQuery_thenMultipleEntityResult() {
Query query = em.createNativeQuery(
"SELECT e.id as idEmployee, e.name, d.id as daysId, d.employeeId, d.dayOfWeek " +
"FROM employee e, schedule_days d " +
"WHERE e.id = d.employeeId",
"EmployeeScheduleResults"
);
List<Object[]> results = query.getResultList();
assertEquals(4, results.size());
assertTrue(results.get(0).length == 2);
Employee emp = (Employee) results.get(1)[0];
ScheduledDay day = (ScheduledDay) results.get(1)[1];
assertTrue(day.getEmployeeId().equals(emp.getId()));
}
✅ 每个结果是一个 Object[]
,按映射顺序包含多个实体实例。
6. 总结
SqlResultSetMapping
是 JPA 中处理原生 SQL 结果映射的“三板斧”:
- ✅
ColumnResult
:简单粗暴,适合只取字段值的场景 - ✅
ConstructorResult
:灵活封装,适合 DTO 或非实体对象 - ✅
EntityResult
:标准规范,适合实体或联表查询
虽然现代开发中 Spring Data JPA + Projection 已能满足大部分需求,但在复杂查询、性能优化或遗留系统对接时,SqlResultSetMapping
依然是不可或缺的利器。
📌 掌握它,能让你在面对“SQL 写得好,但映射不上”的尴尬时,多一份从容。
示例代码已托管至 GitHub:https://github.com/baeldung-tutorials/java-jpa