Using TupleTransformer and ResultListTransformer in Hibernate
In Hibernate, developers often use native SQL queries or JPQL queries that return complex results — such as multiple columns or joined data — which do not directly map to a single entity. Let us delve into understanding Hibernate TupleTransformer and ResultListTransformer and how they help in customizing query result mappings.
1. Introduction
1.1 What is TupleTransformer?
The TupleTransformer interface is used to transform individual rows of query results (known as tuples) into a desired object structure. Each tuple represents one record (row) returned from the query.
public interface TupleTransformer<R> {
R transformTuple(Object[] tuple, String[] aliases);
}
The transformTuple() method is called once for each row (tuple) returned by the query.
- The first parameter,
Object[] tuple, contains the actual column values for that row. - The second parameter,
String[] aliases, provides the column or alias names corresponding to each element in the tuple.
By implementing this interface, you can control how raw query results are converted into your custom domain objects. For example, you can map query results to DTOs (Data Transfer Objects) or aggregate data structures directly instead of working with generic Object[] arrays.
Use case example: When executing a native SQL query that returns columns not mapped directly to an entity, or when you want to combine multiple entity attributes or derived values into a custom projection object. In short, TupleTransformer gives developers low-level control over how each row from a query result is converted before being added to the result list.
1.2 What is ResultListTransformer?
The ResultListTransformer is used to perform transformations at the result list level. It allows developers to post-process or filter the entire result list after all rows are transformed.
public interface ResultListTransformer<R> {
List<R> transformList(List<R> list);
}
The transformList() method is called once after all tuples have been processed by the TupleTransformer (if one is used). This allows for operations that require awareness of the entire result set rather than individual rows.
Typical use cases include removing duplicates from the result list, sorting or reordering the list based on specific business logic, or grouping results and aggregating data across multiple rows. For example, if a query fetches duplicate entries due to a JOIN operation, you can use ResultListTransformer to clean up the final list before returning it to the application layer. In essence, while TupleTransformer handles row-level transformation, ResultListTransformer focuses on list-level post-processing, allowing you to refine the overall result set before it’s returned.
2. Code Example
2.1 Adding Jar Dependencies
Add the following dependencies to the pom.xml file.
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>your__latest__jar__version</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>your__latest__jar__version</version>
</dependency>
2.2 Create Entity Classes
2.2.1 Employee Entity Class
The following code defines the Employee entity class representing the employees table in the database.
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "employees")
public class Employee {
@Id
private Long id;
private String name;
private Double salary;
private Long departmentId;
public Employee() {}
public Employee(Long id, String name, Double salary, Long departmentId) {
this.id = id;
this.name = name;
this.salary = salary;
this.departmentId = departmentId;
}
// getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public Double getSalary() { return salary; }
public Long getDepartmentId() { return departmentId; }
public void setId(Long id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setSalary(Double salary) { this.salary = salary; }
public void setDepartmentId(Long departmentId) { this.departmentId = departmentId; }
}
This entity maps to the employees table and contains basic fields such as id, name, salary, and departmentId. Each instance represents a single employee record, and the annotations ensure Hibernate recognizes it as a persistent entity.
2.2.2 Department Entity Class
The following code defines the Department entity class representing the departments table.
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "departments")
public class Department {
@Id
private Long id;
private String name;
public Department() {}
public Department(Long id, String name) {
this.id = id;
this.name = name;
}
// getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public void setId(Long id) { this.id = id; }
public void setName(String name) { this.name = name; }
}
This entity corresponds to the departments table and includes two fields: id and name. It represents the department information linked with employees through the departmentId field in the Employee entity.
2.3 Create a DTO Class
The following code defines the EmployeeDepartmentDTO class used to hold the combined data from employee and department tables.
package com.example.dto;
import java.util.Objects;
public class EmployeeDepartmentDTO {
private String employeeName;
private String departmentName;
private Double salary;
public EmployeeDepartmentDTO(String employeeName, String departmentName, Double salary) {
this.employeeName = employeeName;
this.departmentName = departmentName;
this.salary = salary;
}
public String getEmployeeName() { return employeeName; }
public String getDepartmentName() { return departmentName; }
public Double getSalary() { return salary; }
@Override
public String toString() {
return employeeName + " (" + departmentName + ") - $" + salary;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof EmployeeDepartmentDTO dto)) return false;
return Objects.equals(employeeName, dto.employeeName)
&& Objects.equals(departmentName, dto.departmentName)
&& Objects.equals(salary, dto.salary);
}
@Override
public int hashCode() {
return Objects.hash(employeeName, departmentName, salary);
}
}
This DTO acts as a data carrier for the query results that combine employee and department details. It includes fields for employee name, department name, and salary, along with overridden equals and hashCode methods to support duplicate removal later in the query result transformation.
2.4 Create a TupleTransformer Implementation
The following code shows how to implement the TupleTransformer to map raw query results into EmployeeDepartmentDTO objects.
package com.example.transformer;
import com.example.dto.EmployeeDepartmentDTO;
import org.hibernate.query.TupleTransformer;
public class EmployeeDeptTupleTransformer implements TupleTransformer {
@Override
public EmployeeDepartmentDTO transformTuple(Object[] tuple, String[] aliases) {
String employeeName = (String) tuple[0];
String departmentName = (String) tuple[1];
Double salary = ((Number) tuple[2]).doubleValue();
return new EmployeeDepartmentDTO(employeeName, departmentName, salary);
}
}
This transformer converts each tuple (row) returned by the native query into a corresponding EmployeeDepartmentDTO object by extracting the employee name, department name, and salary from the query result array.
2.5 Create a Main Class
The following code demonstrates how to configure Hibernate, persist sample data, and use TupleTransformer with ResultListTransformer to fetch and transform query results.
package com.example.demo;
import com.example.model.Employee;
import com.example.model.Department;
import com.example.dto.EmployeeDepartmentDTO;
import com.example.transformer.EmployeeDeptTupleTransformer;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.TupleTransformer;
import java.util.List;
public class HibernateTransformerDemo {
public static void main(String[] args) {
// Create Hibernate configuration
Configuration cfg = new Configuration();
cfg.configure("hibernate.cfg.xml"); // hibernate.cfg.xml must exist in resources
cfg.addAnnotatedClass(Employee.class);
cfg.addAnnotatedClass(Department.class);
SessionFactory sessionFactory = cfg.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
// Sample data insertion (only for demonstration)
session.persist(new Department(1L, "IT"));
session.persist(new Department(2L, "Finance"));
session.persist(new Department(3L, "HR"));
session.persist(new Employee(101L, "John Doe", 55000.0, 2L));
session.persist(new Employee(102L, "Jane Smith", 70000.0, 1L));
session.persist(new Employee(103L, "Robert Brown", 48000.0, 3L));
tx.commit();
// Run query with TupleTransformer + ResultListTransformer
session.beginTransaction();
NativeQuery<EmployeeDepartmentDTO> query = session.createNativeQuery(
"SELECT e.name AS employeeName, d.name AS departmentName, e.salary AS salary " +
"FROM employees e JOIN departments d ON e.department_id = d.id", EmployeeDepartmentDTO.class);
// Apply TupleTransformer
query.setTupleTransformer(new EmployeeDeptTupleTransformer());
// Apply ResultListTransformer to remove duplicates
query.setResultListTransformer(new ResultListTransformer<EmployeeDepartmentDTO>() {
@Override
public List<EmployeeDepartmentDTO> transformList(List<EmployeeDepartmentDTO> list) {
return list.stream().distinct().toList();
}
});
List<EmployeeDepartmentDTO> results = query.getResultList();
// Print results
results.forEach(System.out::println);
session.getTransaction().commit();
session.close();
sessionFactory.close();
}
}
This main class demonstrates the complete workflow: setting up Hibernate configuration, inserting demo data, executing a join query between employees and departments, transforming results into DTOs, and removing duplicates before displaying the final list of employee-department-salary records.
2.6 Create a Hibernate Configuration File
The following XML configuration sets up Hibernate to use an in-memory H2 database and defines essential properties for session management and SQL display. Make sure you have this file under src/main/resources/hibernate.cfg.xml.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.connection.url">jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1</property>
<property name="hibernate.hbm2ddl.auto">create-drop</property>
<property name="hibernate.show_sql">true</property>
</session-factory>
</hibernate-configuration>
This configuration defines a session factory for Hibernate using the H2 in-memory database. The hibernate.dialect specifies H2 SQL dialect, hibernate.hbm2ddl.auto=create-drop automatically creates and drops the schema at runtime, and hibernate.show_sql=true enables SQL query logging for debugging purposes. The database exists only in memory, making it ideal for testing and demo scenarios.
2.7 Code Run and Output
The following section demonstrates running the HibernateTransformerDemo program and displays the expected output after transforming the query results into DTOs and removing duplicates.
package com.example.demo;
public class RunDemo {
public static void main(String[] args) {
HibernateTransformerDemo.main(args);
}
}
This simple runner invokes the main method of HibernateTransformerDemo. When executed, Hibernate connects to the configured database, inserts sample data, executes the native query with TupleTransformer, applies ResultListTransformer to remove duplicates, and prints the resulting employee-department-salary list.
Jane Smith (IT) - $70000.0 John Doe (Finance) - $55000.0 Robert Brown (HR) - $48000.0
The output shows each employee’s name along with their department and salary. The order may vary depending on the database and query execution, but duplicates are removed due to the ResultListTransformer. This demonstrates the successful transformation of query tuples into a structured DTO format for application use.
3. Conclusion
The TupleTransformer and ResultListTransformer APIs in Hibernate provide developers a clean, declarative way to convert query results into domain-specific objects or DTOs. They make it possible to handle complex SQL projections and joins while maintaining a clean separation between data fetching and object mapping logic. Use TupleTransformer for row-level data transformation and ResultListTransformer for list-level processing — together they form a powerful pattern for creating efficient, type-safe data projections.




