HQL запросы и прямая загрузка сущностей, это очень здорово, но хорошо бы иметь озможность и выполнять запросы напрямую, используя всю мощь SQL и вашей базы данных. Однако, такие запросы могут вернуть данные которые Hibernate не ожидает увидеть или которые даже не отображены на существующие сущности. Поэтому для поддержки таких запросов требуется особая реализация.
Простые (скалярные) запросы
Самый простой запрос, это запрос который возвращает какой-либо один столбец. Я использую модель данных из примера с HQL и посчитаю в ней количество людей в базе:
1
2
3
4
|
System.out.println("No persons: "+
session.createSQLQuery("select count(id) as c from Person")
.addScalar("c", IntegerType.INSTANCE)
.uniqueResult());
|
1
|
No persons: 1
|
На SQL запросе я подробно останавливаться не буду, лучше рассмотрю особенности Hibernate. Вызов createSQLQuery() создаёт sql запрос, равно как и createQuery() создаёт HQL запрос. Вызов addScalar("c", IntegerType.INSTANCE) сообщает Hibernate что в результате запроса вернётся простой столбец, не объектб и что он будет типа Integer.
В скалярных запросах можно возвращать и несколько столбцов и при этом вовсе не обязательно указывать их тип. В последнем случае, конечно, разработчику придётся делать приведение типа самому:
1
2
3
|
List<Object[]> passportIds = session.createSQLQuery("select id, passport_id from Person")
.list();
passportIds.forEach(p -> System.out.println("User id: "+p[0]+" Passport id: "+p[1]));
|
1
|
User id: 3 Passport id: 1
|
В это случае возвращается список из массива «сырых» объектов, которые Hibernate и не пытается обрабатывать. Этот подход похож на возврат результатов в Spring JDBC.
Запросы сущностей
Сырые данные из базы это хорошо, но у нас всё таки объектный язык. Результаты SQL запроса можно преобразовать в сущность, выбрав поля, которые её составляют.
1
2
3
4
|
session.createSQLQuery("select p.* from Passport as p, Person as pe where p.id=pe.passport_id and pe.lastName='Testoff'")
.addEntity(Passport.class)
.list()
.forEach(System.out::println);
|
1
|
Passport{series='AS', no='123456', issueDate=2016-07-13, validity=P20Y}
|
Вызов addEntity() указывает Hibernate, какую сущность из данных запроса надо построить. Имена столбцов в ответе должны совпадать с именами, заданными при отображении.
Разумеется, с SQL запросами (и с скалярными и с запросами сущностей) можно использовать стандартный функционал — параметры, пейджинг и т.д.:
1
2
3
4
5
6
7
|
session.createSQLQuery("select * from Person as p join Passport as pa on p.passport_id=pa.id and p.lastName = :name")
.addEntity("p",Person.class)
.addJoin("pa", "p.passport")
.setResultTransformer( Criteria.ROOT_ENTITY )
.setString("name", "Testoff")
.list()
.forEach(System.out::println);
|
Запросы выше кроме использования параметров показывает, как загружать связанные объъекты. Вызов addEntity() указывает Hibernate, какой объект мы загружаем (корневой объект), вызов(ы) addJoin() говорят Hibernate, какие связанные объекты загружаются вместе с корневым и как они к нему относятся.
Преобразование результатов
Вызов setResultTransformer() в предыдущем примере говорит Hibernate, что мы хотим получить конкретно экземпляр корневого объекта, а не набор загруженных данных. Но это не единственное и не главное предназначение setResultTransformer(). Если говорить обще, этот вызов позволяет задать код, который будет преобразовывать загруженные из базы данных. Один из вариантов использования этого функционала — загрузка данных во временные объекты, которые даже не имеют отображения (DTO — data transfer objects)
Для начала я создам такой DTO:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* Company name extractor
*/
@AllArgsConstructor
@NoArgsConstructor
public class CompanyNameDTO {
/**
* The name.
*/
@Getter
@Setter
private String name;
}
|
Для использования DTO с ResultTransfomer он должен иметь конструктор по умолчанию и сеттеры на полях. Затем мы трансформируем результат запроса в DTO:
1
2
3
4
|
session.createSQLQuery("select name as \"name\"from Company")
.setResultTransformer(Transformers.aliasToBean(CompanyNameDTO.class))
.list()
.forEach(System.out::println);
|
1
|
CompanyNameDTO(name=Acme Ltd)
|
Выражение name as \"name\" в данном случае критично важно, так как имя столбца в ответе должно совпадать с именем поля в DTO.
Именованные запросы
Именованные запросы с SQL работают так же, как и с HQL и даже используют тот же вызов. Но определение запроса меняется: во-первых аннотация теперь называется @NamedNativeQuery, во-вторых необходимо явно задавать тип возвращаемой сущности:
1
2
3
4
5
6
7
8
9
|
@NamedNativeQueries({
@NamedNativeQuery(
name = "findCompanyWithName",
query = "select * from Company where name like :name",
resultClass = Company.class
)
})
public class Company extends AbstractIdentifiableObject {
}
|
1
2
3
4
|
session.getNamedQuery("findCompanyWithName")
.setParameter("name", "Ac%")
.list()
.forEach(System.out::println);
|
1
|
Company{name='Acme Ltd', workers=Test}
|
В остальном именованные SQL запросы ведут себя так же, как и именованные HQL запросы и поддерживают тот же самый функционал.
Кроме того, именованные SQL запросы поддерживают и DTO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@NamedNativeQueries({
@NamedNativeQuery(
name = "findCompanyNameOnly",
query = "select name from Company",
resultSetMapping = "company_name_dto"
)
})
@SqlResultSetMapping(
name = "company_name_dto",
classes = @ConstructorResult(
targetClass = CompanyNameDTO.class,
columns = {
@ColumnResult(name="name")
}
)
)
public class Company extends AbstractIdentifiableObject {
}
|
1
2
3
|
session.getNamedQuery("findCompanyNameOnly")
.list()
.forEach(System.out::println);
|
1
|
CompanyNameDTO(name=Acme Ltd)
|
Для использования DTO и именованных SQL запросов требуется определить соответствующее отображение аннотацией @SqlResultSetMapping и связать его с именованным запросом. Очевидно, что одно отображение может быть использовано в нескольких запросах.
Для того, чтобы DTO можно быть использовать с @ConstructorResult, класс DTO должен иметь конструктор, инициализирующий его значения.
Код примера доступен на github.