Мы уже рассматривали несколько способов получить проекцию. Самым гибким считается 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 все так же будет).