Если по тем или иным причинам нет возможности создать полноценное представление прямо в БД, то Hibernate позволяет создать суррогат такого представления с помощью аннотации @org.hibernate.annotations.Subselect прямо в приложении.
Создаваемый таким образом суррогат представления будет зависеть от конкретной БД, так как SELECT для этого представления должен быть написан на SQL диалекте используемой БД. Это, конечно, уменьшает переносимость такого кода.
Чтобы проиллюстрировать создание представления выполним следующие действия.
Создадим минимальное Spring Boot приложение с поддержкой JPA/Hibernate
Создадим два класса предметной области (каждый в своём файле):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Entity public class UserGroup { @Id @GeneratedValue private Long id; private String number; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } @Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Long groupId; //Конструкторы, геттеры и сеттеры, equals(), hashCode() и т.д. } |
Для простоты мы не будем использовать возможности Hibernate устанавливать связи один ко многим у классов предметной области.
Создадим специальную сущность, в которой будет содержаться SQL запрос для формирования представления внутри приложения, а также необходимый набор полей.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Entity @org.hibernate.annotations.Immutable @org.hibernate.annotations.Subselect( "select p.id as id, p.name as person_name, g.number as group_number " + "from person p left join user_group g on g.id = p.group_id" ) @org.hibernate.annotations.Synchronize({"PERSON", "USER_GROUP"}) public class PersonWithGroupName { @Id Long id; private String personName; private String groupNumber; |
Во-первых, сущность помечается аннотацией @org.hibernate.annotations.Immutable, что логично для сущности соответствующей представлению (view).
Во второй аннотации, @org.hibernate.annotations.Subselect, мы указываем SQL запрос, на основании которого будет формироваться представление. Важно следить, чтобы имена получающихся колонок представления — в нашем случае id, person_name и group_number — соответствовали именам полей класса с учётом всех стандартных или кастомных (если они есть) преобразований. В противном случае отображение строк в объекты работать не будет.
Так у нас колонка id в запросе соответствует полю id в классе, person_name в запросе соответствует personName в классе с учётом стандартных преобразований имён полей в имена колонок, ну и group_number соответствует groupNumber.
В третью аннотацию, @org.hibernate.annotations.Synchronize, передаётся массив имён таблиц (именно таблиц, как они есть в БД), которые участвуют в формировании представления. Если этого не сделать или указать не все таблицы, то возникнет вероятность, что в представлении окажутся неактуальные данные.
Создадим для всех трёх сущностей репозитории (каждый в своём файле):
1 2 3 4 5 6 7 8 9 10 11 |
@Repository public interface UserGroupRepository extends JpaRepository<UserGroup, Long> { } @Repository public interface PersonRepository extends JpaRepository<Person, Long> { } @Repository public interface PersonWithGroupNameRepository extends JpaRepository<PersonWithGroupName, Long> { } |
Создадим тест, в котором проверим, что с помощью репозитория PersonWithGroupNameRepository можно извлечь данные из таблиц «PERSON» и «USER_GROUP», как если бы в БД существовало соответствующее представление:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@SpringBootTest class TestApplicationTests { @Autowired PersonRepository personRepository; @Autowired UserGroupRepository userGroupRepository; @Autowired PersonWithGroupNameRepository personWithGroupNameRepository; @Test void testSubselect() { UserGroup martins = userGroupRepository.save(new UserGroup("Стрижи")); personRepository.saveAll(List.of( new Person("Владимир", martins.getId()), new Person("Ирина", martins.getId()) )); List<PersonWithGroupName> viewData = personWithGroupNameRepository.findAll(); viewData.sort(Comparator.comparing(PersonWithGroupName::getPersonName)); assertEquals(viewData.size(), 2); assertEquals(viewData.get(0).getPersonName(), "Владимир"); assertEquals(viewData.get(1).getPersonName(), "Ирина"); } } |
Сперва мы создаём одну строку в таблице USER_GROUP и две строки в таблице PERSON с groupId созданной группы.
Затем происходит самое интересное: мы вызываем стандартный в Spring Data JPA метод findAll() на бине репозитория PersonWithGroupNameRepository и он отрабатывает так, будто в БД есть представление (view), связывающее две таблицы и мы делаем запрос к нему. Хотя в БД никакого представления нет, оно существует только на уровне Hibernate приложения.
Затем мы сортируем выборку, чтобы результаты ассертов были предсказуемыми в плане порядка следования элементов и убеждаемся, что в суррогатном представлении есть две строки, как мы и ожидаем.
Использование суррогатных представлений на уровне приложения может быть очень удобно, когда нет возможности создать реальное представление внутри БД.