jpa-sql-resultset-mapping
A Guide to SqlResultSetMapping
1. Introduction
The core functionality here involves mapping result sets from database SQL statements into Java objects.
2. Setup
2.1. Maven Dependency
Our required Maven dependencies are Hibernate and H2 Database. Hibernate gives us the implementation of the JPA specification. We use H2 Database for an in-memory database.
2.2. Database
CREATE TABLE EMPLOYEE
(id BIGINT,
name VARCHAR(10));
The EMPLOYEE table stores one result Entity object. SCHEDULE_DAYS contains records linked to the EMPLOYEE table by the column employeeId:
CREATE TABLE SCHEDULE_DAYS
(id IDENTITY,
employeeId BIGINT,
dayOfWeek VARCHAR(10));
A script for data creation can be found in the code for this guide.
2.3. Entity Objects
@Entity
public class Employee {
@Id
private Long id;
private String name;
}
Entity objects might be named differently than database tables. We can annotate the class with @Table to explicitly map them:
@Entity
@Table(name = "SCHEDULE_DAYS")
public class ScheduledDay {
@Id
@GeneratedValue
private Long id;
private Long employeeId;
private String dayOfWeek;
}
3. Scalar Mapping
3.1. ColumnResult
While SqlResultSetMapping and Query annotations work on Repository classes as well, we use the annotations on an Entity class in this example.
Every SqlResultSetMapping annotation requires only one property, name. However, without one of the member types, nothing will be mapped. The member types are ColumnResult, ConstructorResult, and EntityResult.
In this case, ColumnResult maps any column to a scalar result type:
@SqlResultSetMapping(
name="FridayEmployeeResult",
columns={@ColumnResult(name="employeeId")})
The ColumnResult property name identifies the column in our query:
@NamedNativeQuery(
name = "FridayEmployees",
query = "SELECT employeeId FROM schedule_days WHERE dayOfWeek = 'FRIDAY'",
resultSetMapping = "FridayEmployeeResult")
Note that the value of resultSetMapping in our NamedNativeQuery annotation is important because it matches the name property from our ResultSetMapping declaration.
As a result, the NamedNativeQuery result set is mapped as expected. Likewise, StoredProcedure API requires this association.
3.2. ColumnResult Test
@BeforeAll
public static void setup() {
emFactory = Persistence.createEntityManagerFactory("java-jpa-scheduled-day");
em = emFactory.createEntityManager();
}
Finally, we call the named query to run our test:
@Test
public void whenNamedQuery_thenColumnResult() {
List<Long> employeeIds = em.createNamedQuery("FridayEmployees").getResultList();
assertEquals(2, employeeIds.size());
}
4. Constructor Mapping
4.1. ConstructorResult
Similarly to our ColumnResult example, we will add the SqlResultMapping annotation on our Entity class, ScheduledDay. However, in order to map using a constructor, we need to create one:
public ScheduledDay (
Long id, Long employeeId,
Integer hourIn, Integer hourOut,
String dayofWeek) {
this.id = id;
this.employeeId = employeeId;
this.dayOfWeek = dayofWeek;
}
Also, the mapping specifies the target class and columns (both required):
@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")})})
The order of the ColumnResults is very important. If columns are out of order the constructor will fail to be identified. In our example, the ordering matches the table columns, so it would actually not be required.
@NamedNativeQuery(name = "Schedules",
query = "SELECT * FROM schedule_days WHERE employeeId = 8",
resultSetMapping = "ScheduleResult")
Another unique difference for ConstructorResult is that the resulting object instantiation as “new” or “detached”. The mapped Entity will be in the detached state when a matching primary key exists in the EntityManager otherwise it will be new.
Sometimes we may encounter runtime errors because of mismatching SQL datatypes to Java datatypes. Therefore, we can explicitly declare it with type.
4.2. ConstructorResult Test
@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() == 3));
}
5. Entity Mapping
5.1. Single Entity
EntityResult requires us to specify the entity class, Employee. We use the optional fields property for more control. Combined with FieldResult, we can map aliases and fields that do not match:
@SqlResultSetMapping(
name="EmployeeResult",
entities={
@EntityResult(
entityClass = com.baeldung.sqlresultsetmapping.Employee.class,
fields={
@FieldResult(name="id",column="employeeNumber"),
@FieldResult(name="name", column="name")})})
Now our query should include the aliased column:
@NamedNativeQuery(
name="Employees",
query="SELECT id as employeeNumber, name FROM EMPLOYEE",
resultSetMapping = "EmployeeResult")
Similarly to ConstructorResult, EntityResult requires a constructor. However, a default one works here.
5.2. Multiple Entities
@SqlResultSetMapping(
name = "EmployeeScheduleResults",
entities = {
@EntityResult(entityClass = com.baeldung.sqlresultsetmapping.Employee.class),
@EntityResult(entityClass = com.baeldung.sqlresultsetmapping.ScheduledDay.class)
5.3. EntityResult Tests
@Test
public void whenNamedQuery_thenSingleEntityResult() {
List<Employee> employees = Collections.checkedList(
em.createNamedQuery("Employees").getResultList(), Employee.class);
assertEquals(3, employees.size());
assertTrue(employees.stream().allMatch(c -> c.getClass() == Employee.class));
}
Since the multiple entity results join two entities, the query annotation on only one of the classes is confusing.
For that reason, we define the query in the test:
@Test
public void whenNamedQuery_thenMultipleEntityResult() {
Query query = em.createNativeQuery(
"SELECT e.id, e.name, d.id, 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() == emp.getId());
}
6. Conclusion
In this guide, we looked at different options for using the SqlResultSetMapping annotation. SqlResultSetMapping is a key part to the Java Persistence API.
Code snippets can be found over on GitHub.