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. 数据库表结构

我们创建两张表:EMPLOYEESCHEDULE_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 的值必须与 SqlResultSetMappingname 完全一致,否则会抛异常。


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


原始标题:A Guide to SqlResultSetMapping | Baeldung