Мы уже рассматривали несколько способов получить проекцию. Самым гибким считается Hibernate ResultTransformer. Его мы и будем использовать ниже для получения проекции OneToMany.
Модель
Итак, допустим у нас есть City (город) и District (район) в отношении OneToMany:
@Data @NoArgsConstructor @Entity public class City { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; private String code; @OneToMany( cascade = CascadeType.ALL, mappedBy = "city", orphanRemoval = true) private List<District> districts=new ArrayList<>(); ... } @Data @NoArgsConstructor @Entity public class District { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; private String code; @ManyToOne private City city; ... }
Это сущности, допустим у них много избыточных полей.
Проекция
Все, что нам надо извлечь из базы — это CityDto и DistrictDto с ограниченным набором полей, то есть проекции. Которые конечно тоже находятся отношении OneToMany:
@Data @AllArgsConstructor @NoArgsConstructor public class CityDto { public static final String ID_ALIAS = "c_id"; public static final String NAME_ALIAS = "c_name"; private long id; private String name; private List<DistrictDto> districtDtoList=new ArrayList<>(); public CityDto( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = ((Number) tuples[aliasToIndexMap.get(ID_ALIAS)]).longValue(); this.name = tuples[aliasToIndexMap.get(NAME_ALIAS)].toString(); } }
Обратите внимание, это не @Entity, а просто классы (с особым конструктором, он понадобится ниже).
Класс DistrictDto:
@Data @NoArgsConstructor @AllArgsConstructor public class DistrictDto { public static final String ID_ALIAS = "d_id"; public static final String NAME_ALIAS = "d_name"; private long id; private String name; public DistrictDto( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = ((Number) tuples[aliasToIndexMap.get(ID_ALIAS)]).longValue(); this.name = tuples[aliasToIndexMap.get(NAME_ALIAS)].toString(); } }
Получение проекции с помощью ResultTransformer
Чтобы получить CityDto c коллекцией DistrictDto, напишем запрос:
List<CityDto> cityDtos = em.createQuery("select c.id as c_id,\n" + " c.name as c_name,\n" + " d.id as d_id,\n" + " d.name as d_name\n" + " from District d\n" + " join d.city c\n" + " order by d.id") .unwrap(org.hibernate.query.Query.class) .setResultTransformer(new CityDtoResultTransformer()) .getResultList();
Как видно выше, в мы задаем CityDtoResultTransformer, который преобразует результат запроса к нужному нам виду.
Выглядит он так.
CityDtoResultTransformer
public class CityDtoResultTransformer implements ResultTransformer { private Map<Long, CityDto> cityDTOMap = new LinkedHashMap<>(); @Override public Object transformTuple( Object[] tuple, String[] aliases) { Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases); Long cityId= ((Number) tuple[aliasToIndexMap.get(CityDto.ID_ALIAS)]).longValue(); CityDto cityDto = cityDTOMap.computeIfAbsent( cityId, id -> new CityDto(tuple, aliasToIndexMap) ); cityDto.getDistrictDtoList().add( new DistrictDto(tuple, aliasToIndexMap) ); return cityDto; } @Override public List transformList(List collection) { return new ArrayList(cityDTOMap.values()); } public Map<String, Integer> aliasToIndexMap( String[] aliases) { Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>(); for (int i = 0; i < aliases.length; i++) { aliasToIndexMap.put(aliases[i], i); } return aliasToIndexMap; } }
CityDtoResultTransformer реализует интерфейс ResultTransformer и применяется к каждой строке результата SQL-запроса (в такой SQL преобразуется приведенный выше JPQL):
select c.id as c_id, c.name as c_name, d.id as d_id, d.name as d_name from district d join city c on c.id=d.city_id order by d.id
Результат содержит повторяющиеся City:
Поэтому cityDto вычисляем и добавляем в Map, только если его там еще нет:
cityDTOMap.computeIfAbsent
Аргументы метода transformTuple():
- tuple[] — массив значений полей, например: 5, city5, -120, d10 из таблицы выше
- aliases[] — массив названий полей — c_id, c_name, d_id, d_name.
aliasToIndexMap — этот мы делаем мы сами, он содержит алиасы полей и соответствующие им индексы в tuple[]. Например: [«c_id», 0], [«c_name», 1].
Итоги
Мы реализовали свой ResultTransformer , чтобы получить проекцию OneToMany. Исходный код есть на GitHub.
А если в классе (помимо @OneToMany и @ManyToOne) присутствует также @ManyToMany, есть какие-то варианты работы с таким Entity-классом с помощью проекций, @SqlResultSetMapping или ResultTransformer? При этом данные для этого Entity-класса получаются из БД с помощью native-SQL в JPA-репозитории.
Можно ResultTransformer написать, только он сложнее будет (не потому, что ManyToMany сложнее OneToMany), а потому что три вложенных объекта формировать сложнее, чем один (не один список будет, а два).
Пример с ManyToMany тут. (с native query все так же будет).