В этой статье пойдет речь о том, как превратить результат нативного SQL-запроса в объект.
Введение
SQL-запрос дает больше возможностей, чем JPQL-запрос, но его результат труднее обработать (превратить в объект). В общем случае возвращается список записей, каждая из которых — массив. И в массиве столько элементов, сколько выбрано столбцов, а каждый столбец нужно преобразовывать к своему типу.
Ниже пример.
Преобразование SQL-запроса в общем случае (без маппинга)
Пусть есть таблица user:

Мы запрашиваем из нее всё:
List<Object[]> users = this.em.createNativeQuery(
"select id, email, nickname, password, role, locked from user ")
.getResultList()
for (Object[] user : users) {
long id = ((BigInteger) user[0]).longValue();
String email = (String) user[1];
String nickname = (String) user[2];
String password = (String) user[3];
String role = (String) user[4];
boolean locked = (Boolean) user[5];
User user1=new User(id, email, nickname, password, role, locked);
...
}
Как видите, каждая строка — массив, а поле id возвращается как BigInteger, который нужно вручную преобразовать в long. А затем из полей построить объект User (если необходимо).
Класс User:
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(generator = "sequence")
private Long id;
private String email;
private String nickname;
private String password;
private String role = "ROLE_USER";
private boolean locked = false;
}
Впрочем, поскольку в данном запросе мы просто получаем все поля из таблицы user, и User является сущностью, то можно преобразовать результат в User автоматически.
Автоматическое преобразование простого запроса
Для этого вторым параметром createNativeQuery() нужно указать User.class:
List<User> list = this.em.createNativeQuery(
"select u.id as userid, u.email, u.nickname, u.password, u.role, u.locked from user u",
User.class).getResultList();
for (User user : users) {
System.out.println(user.getEmail());
}
Cоответственно возвращаемый тип будет уже List<User>, и не List<Object[]>.
Но такое автоматическое преобразование возможно не для всех запросов.
В более сложных случаях нужно задать сопоставление в @SqlResultSetMapping.
@SqlResultSetMapping
Например, пусть необходимо получить данные не из одной, а из двух таблиц — user и post — таблицы пользователей и их постов.

Пост и пользователь находятся в отношение ManyToOne. Сущность User уже приведена выше, а Post выглядит так:
@Entity
@Data
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue(generator = "sequence")
private Long id;
private String title;
private String text;
@ManyToOne
private User user;
}
Результаты выборки из этих двух таблиц могут быть разными объектами (в зависимости от того, что мы хотим получить):
- некий DTO-объект (например, перечень заголовков постов и с именем пользователя);
- сущность и скалярный столбец (список пользователей с количеством постов для каждого пользователя).
Здесь автоматическое преобразование не работает, нужно задавать сопоставление в @SqlResultSetMapping. Исходный код аннотации:
@Repeatable(SqlResultSetMappings.class)
@Target({TYPE})
@Retention(RUNTIME)
public @interface SqlResultSetMapping {
String name();
EntityResult[] entities() default {};
ConstructorResult[] classes() default {};
ColumnResult[] columns() default {};
}
Как видите, помимо имени, в ней можно задать сущности, конструкторы для DTO-объектов и скалярные столбцы. В любой комбинации. Ниже рассмотрим примеры использования.
ConstructorResult
Получим DTO-объекты PostDto — перечень постов с именем пользователя:
@Data
@AllArgsConstructor
public class PostDto {
private long id;
private String title;
private long userId;
private String nickname;
}
SQL-запрос выглядит так:
select p.id, p.title, u.id as userId, u.nickName from post as p join user as u on p.user_id=u.id
Чтобы сопоставить результат SQL-запроса объекту PostDto, в ConstructorResult пропишем названия столбцов, которые идут в конструктор PostDto:
@SqlResultSetMapping(name = "PostDtoMapping",
classes = {
@ConstructorResult(
columns = {
@ColumnResult(name = "id", type = long.class),
@ColumnResult(name = "title"),
@ColumnResult(name = "userId", type = long.class),
@ColumnResult(name = "nickname")
},
targetClass = PostDto.class
)}
)
Обратите внимание, что поскольку есть два столбца с названием id, в SQL мы прописали алиас u.id as userId и использовали его @ConstructorResult.
Аннотация прописывается в классе сущности, а сам SQL-запрос можно задать как внутри em.CreateNativeQuery(), так и прописать его в классе сущности под своим именем.
Пропишем сам запрос тоже в классе сущности:
@Entity
@Data
@NoArgsConstructor
@NamedNativeQuery(name = "PostDtos", query = "select p.id, p.title, " +
"u.id as userId, u.nickName " +
"from post as p join user as u on p.user_id=u.id ", resultSetMapping = "PostDtoMapping")
@SqlResultSetMapping(name = "PostDtoMapping",
classes = {
@ConstructorResult(
columns = {
@ColumnResult(name = "id", type = long.class),
@ColumnResult(name = "title"),
@ColumnResult(name = "userId", type = long.class),
@ColumnResult(name = "nickname")
},
targetClass = PostDto.class
)}
)
public class Post {
@Id
@GeneratedValue(generator = "sequence")
private Long id;
private String title;
private String text;
@ManyToOne
private User user;
}
В @NamedNativeQuery указано имя нашего маппинга — PostDtoMapping.
И теперь в кастомном репозитории создадим метод, возвращающий List<PostDto>, внутри вместо самого запроса указываем его имя — PostDtos:
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<PostDto> findPostDtos() {
List<PostDto> results=em.createNamedQuery("PostDtos").getResultList();
return results;
}
}
Как уже сказано, можно было задать запрос и в аннотации @Query:
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
@Query(nativeQuery = true, name = "PostDtos")
List<PostDto> findDtos();
}
EntityResult и ColumnResult
Теперь получим список пользователей с количеством постов. Причем у пользователя извлекаем все данные. То есть результат будет сущность User и скалярный столбец. Поэтому подойдет EntityResult (для сущность User) и ColumnResult (столбец с количеством постов)
SQL такой:
select count(p.id) as post_count, u.* from user as u left join post as p on p.user_id=u.id group by u.id
Пропишем его в сущности User, и там же @SqlResultSetMapping:
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@NamedNativeQuery(name = "UsersWithPostCount",
query = "select count(p.id) as post_count, " +
"u.* " +
"from user as u left join post as p on p.user_id=u.id group by u.id",
resultSetMapping = "UsersWithPostCountMapping")
@SqlResultSetMapping(name = "UsersWithPostCountMapping",
entities = @EntityResult(
entityClass = User.class,
fields = {
@FieldResult(name = "id", column = "id"),
@FieldResult(name = "email", column = "email"),
@FieldResult(name = "nickname", column = "nickname"),
@FieldResult(name = "password", column = "password"),
@FieldResult(name = "role", column = "role"),
@FieldResult(name = "locked", column = "locked"),
}
),
columns = @ColumnResult(
name = "post_count",
type = int.class
)
)
public class User {
@Id
@GeneratedValue(generator = "sequence")
private Long id;
private String email;
private String nickname;
private String password;
private String role = "ROLE_USER";
private boolean locked = false;
}
Теперь создадим кастомный репозиторий для метода (но можно было бы прописать в @Query):
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<Tuple> findUsersWithPostCount() {
List<Tuple> results = this.em.createNamedQuery("UsersWithPostCount").getResultList();
return results;
}
}
Теперь мы получаем Tuple, который состоит из двух объектов — User и Integer.
Напишем тест:
@Test
public void shouldFindUsersWithPostCount() {
List<Tuple> users = userRepository.findUsersWithPostCount();
for (Tuple tuple : users) {
User user = tuple.get(0, User.class);
int count = tuple.get(1, Integer.class);
assertNotNull(user);
assertTrue(count>=0);
}
assertEquals(2, users.size());
}
Можно было бы вернуть List<Object[]> вместо List<Tuple>.
Итоги
@SqlResultSetMapping позволяет декларативно задать преобразование результата SQL-запроса в объект(ы). Мы рассмотрели, как с помощью нее получить сущности, DTO-объекты и скалярные столбцы (в любой комбинации). Но вложенные объекты с помощью этой аннотации задать нельзя.
@SqlResultSetMapping — JPA-аннотация, и в Hibernate ей соответствует разве что интерфейс ResultTransformer. Он тоже предназначен для преобразования результата запроса, но, в отличие от аннотации, интерфейс ResultTransformer более гибкий: с ним можно получить объекты с вложенными dto-объектами. Кроме того, ResultTransformer подходит для преобразования результата не только нативного SQL, но и JPQL.
Зато @SqlResultSetMapping компактнее.
Исходный код примера есть на GitHub.