Здесь напишем более сложный ResultTransformer, чем в предыдущем примере. Помимо коллекции @OneToMany сущность будет содержать еще коллекцию @ManyToMany.
Кроме того, попробуем получить результат еще одним способом — без ResultTransformer, а с помощью операций над Stream.
Сущности и проекции
Итак, задача — получить проекцию с вложенными объектами. Сейчас у нас три сущности — City, District и Shop.
@GeneratedValue(strategy = GenerationType.SEQUENCE)
cascade = CascadeType.ALL,
private List<District> districts=new ArrayList<>();
private List<Shop> shops=new ArrayList<>();
//setters/getters/constructors/equals/hashcode
@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<>();
@ManyToMany
private List<Shop> shops=new ArrayList<>();
//setters/getters/constructors/equals/hashcode
}
@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<>();
@ManyToMany
private List<Shop> shops=new ArrayList<>();
//setters/getters/constructors/equals/hashcode
}
Задача — получить из базы не сущность City с вложенными коллекциями District и Shop, а dto-объекты CityDto c вложенными коллекциями dto-объектов DistrictDto и ShopDto:
public static final String ID_ALIAS = "c_id";
public static final String NAME_ALIAS = "c_name";
private List<DistrictDto> districtDtoList=new ArrayList<>();
private List<ShopDto> shopDtoList=new ArrayList<>();
public CityDto(long id, String name){
//setters/getters/equals/hashcode
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<>();
private List<ShopDto> shopDtoList=new ArrayList<>();
public CityDto(long id, String name){
this.id=id;
this.name=name;
}
//setters/getters/equals/hashcode
}
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<>();
private List<ShopDto> shopDtoList=new ArrayList<>();
public CityDto(long id, String name){
this.id=id;
this.name=name;
}
//setters/getters/equals/hashcode
}
JPQL-запрос
Чтобы вытащить вместе c City как shops, так и districts, необходимо два join:
List<CityDto> cityDtos = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" from City c left join c.shops s join c.districts d \n" +
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new CityDtoResultTransformer())
List<CityDto> cityDtos = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" s.id as s_id,\n" +
" s.name as s_name,\n" +
" d.id as d_id,\n" +
" d.name as d_name\n" +
" from City c left join c.shops s join c.districts d \n" +
" order by c.id")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new CityDtoResultTransformer())
.getResultList();
List<CityDto> cityDtos = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" s.id as s_id,\n" +
" s.name as s_name,\n" +
" d.id as d_id,\n" +
" d.name as d_name\n" +
" from City c left join c.shops s join c.districts d \n" +
" order by c.id")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new CityDtoResultTransformer())
.getResultList();
Возвращаемый список — не List<City>, а List<CityDto>, причем каждый CityDto будет содержать обе вложенные коллекции. Возможно это благодаря CityDtoResultTransformer, который преобразует каждую полученную строку.
CityDtoResultTransformer
Мы уже рассматривали ResultTransformer, вытаскивающий одну коллекцию. А теперь он будет вытаскивать две коллекции — не только districts, но и shops. (В прошлом примере shops не было.)
Тут два метода:
- метод transformTuple() применяется к каждой строке результата и наполняет cityDTOMap
- А метод transformList() применяется один раз — он преобразует cityDTOMap в List<CityDto>, который и возвращается в итоге.
public class CityDtoResultTransformer implements ResultTransformer {
private Map<Long, CityDto> cityDTOMap = new LinkedHashMap<>();
public Object transformTuple(
List<String> aliasList = Arrays.asList(aliases);
Map<String, Object> tupleMap = aliasList.stream()
HashMap::new, (m, a) -> m.put(a, tuples[aliasList.indexOf(a)]), HashMap::putAll);
Long cityId = (Long) tupleMap.get(CityDto.ID_ALIAS);
String cityName = (String) tupleMap.get(CityDto.NAME_ALIAS);
CityDto cityDto = cityDTOMap.computeIfAbsent(
id -> new CityDto(cityId, cityName)
if (tupleMap.get(ShopDto.ID_ALIAS) != null) {
ShopDto shopDto = new ShopDto((Long) tupleMap.get(ShopDto.ID_ALIAS), (String) tupleMap.get(ShopDto.NAME_ALIAS));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
if (tupleMap.get(DistrictDto.ID_ALIAS) != null) {
DistrictDto districtDto = new DistrictDto((Long) tupleMap.get(DistrictDto.ID_ALIAS), (String) tupleMap.get(DistrictDto.NAME_ALIAS));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
public List transformList(List collection) {
return new ArrayList(cityDTOMap.values());
public class CityDtoResultTransformer implements ResultTransformer {
private Map<Long, CityDto> cityDTOMap = new LinkedHashMap<>();
@Override
public Object transformTuple(
Object[] tuples,
String[] aliases) {
List<String> aliasList = Arrays.asList(aliases);
Map<String, Object> tupleMap = aliasList.stream()
.collect(
HashMap::new, (m, a) -> m.put(a, tuples[aliasList.indexOf(a)]), HashMap::putAll);
Long cityId = (Long) tupleMap.get(CityDto.ID_ALIAS);
String cityName = (String) tupleMap.get(CityDto.NAME_ALIAS);
CityDto cityDto = cityDTOMap.computeIfAbsent(
cityId,
id -> new CityDto(cityId, cityName)
);
if (tupleMap.get(ShopDto.ID_ALIAS) != null) {
ShopDto shopDto = new ShopDto((Long) tupleMap.get(ShopDto.ID_ALIAS), (String) tupleMap.get(ShopDto.NAME_ALIAS));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
}
if (tupleMap.get(DistrictDto.ID_ALIAS) != null) {
DistrictDto districtDto = new DistrictDto((Long) tupleMap.get(DistrictDto.ID_ALIAS), (String) tupleMap.get(DistrictDto.NAME_ALIAS));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
}
return cityDto;
}
@Override
public List transformList(List collection) {
return new ArrayList(cityDTOMap.values());
}
}
public class CityDtoResultTransformer implements ResultTransformer {
private Map<Long, CityDto> cityDTOMap = new LinkedHashMap<>();
@Override
public Object transformTuple(
Object[] tuples,
String[] aliases) {
List<String> aliasList = Arrays.asList(aliases);
Map<String, Object> tupleMap = aliasList.stream()
.collect(
HashMap::new, (m, a) -> m.put(a, tuples[aliasList.indexOf(a)]), HashMap::putAll);
Long cityId = (Long) tupleMap.get(CityDto.ID_ALIAS);
String cityName = (String) tupleMap.get(CityDto.NAME_ALIAS);
CityDto cityDto = cityDTOMap.computeIfAbsent(
cityId,
id -> new CityDto(cityId, cityName)
);
if (tupleMap.get(ShopDto.ID_ALIAS) != null) {
ShopDto shopDto = new ShopDto((Long) tupleMap.get(ShopDto.ID_ALIAS), (String) tupleMap.get(ShopDto.NAME_ALIAS));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
}
if (tupleMap.get(DistrictDto.ID_ALIAS) != null) {
DistrictDto districtDto = new DistrictDto((Long) tupleMap.get(DistrictDto.ID_ALIAS), (String) tupleMap.get(DistrictDto.NAME_ALIAS));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
}
return cityDto;
}
@Override
public List transformList(List collection) {
return new ArrayList(cityDTOMap.values());
}
}
Тут aliases — список названий полей (например, c_id, c_name) из запроса. А tuples — список объектов в строке (1, «name» и т.д.). У нас в запросе по 6 штук.
tupleMap — по алиасу выдает объект (например, по «c_id» выдает 1).
computeIfAbsent() исключает повторяющиеся CityDto. Это нужно, так как join, (а тем более два join), выдает сразу несколько строк для одного CityDto.
Обратите внимание, что используется left join — это значит, справа могу быть нули. Отсюда проверка на null при формировании ShopDto и DistrictDto . Мы из создаем и добавляем в соответствующие списки в объекте CityDto.
Без ResultTransformer
Как было сказано вначале, можно обойтись и без ResultTransformer. Ту же логику придется прописать после получения списка (а лучше потока Stream) Tuple.
Итак, запрос такой же, но возвращается теперь Stream<Tuple>:
Stream<Tuple> resultStream = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" from City c left join c.shops s join c.districts d \n" +
" order by c.id", Tuple.class)
Stream<Tuple> resultStream = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" s.id as s_id,\n" +
" s.name as s_name,\n" +
" d.id as d_id,\n" +
" d.name as d_name\n" +
" from City c left join c.shops s join c.districts d \n" +
" order by c.id", Tuple.class)
.getResultStream();
Stream<Tuple> resultStream = em.createQuery(" select c.id as c_id, c.name as c_name,\n" +
" s.id as s_id,\n" +
" s.name as s_name,\n" +
" d.id as d_id,\n" +
" d.name as d_name\n" +
" from City c left join c.shops s join c.districts d \n" +
" order by c.id", Tuple.class)
.getResultStream();
Код для получения List<CityDto>:
Map<Long, CityDto> cityDtoMap = new LinkedHashMap<>();
List<CityDto> cityDtos = resultStream
CityDto cityDto = cityDtoMap.computeIfAbsent(tuple.get("c_id", Long.class),
tuple.get("c_id", Long.class),
tuple.get("c_name", String.class)
if (tuple.get("d_id") != null) {
DistrictDto districtDto = new DistrictDto(
tuple.get("d_id", Long.class),
tuple.get("d_name", String.class));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
if (tuple.get("s_id") != null) {
ShopDto shopDto = new ShopDto(
tuple.get("s_id", Long.class),
tuple.get("s_name", String.class));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
.collect(Collectors.toList());
Map<Long, CityDto> cityDtoMap = new LinkedHashMap<>();
List<CityDto> cityDtos = resultStream
.map(tuple -> {
CityDto cityDto = cityDtoMap.computeIfAbsent(tuple.get("c_id", Long.class),
id -> new CityDto(
tuple.get("c_id", Long.class),
tuple.get("c_name", String.class)
)
);
if (tuple.get("d_id") != null) {
DistrictDto districtDto = new DistrictDto(
tuple.get("d_id", Long.class),
tuple.get("d_name", String.class));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
}
if (tuple.get("s_id") != null) {
ShopDto shopDto = new ShopDto(
tuple.get("s_id", Long.class),
tuple.get("s_name", String.class));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
}
return cityDto;
})
.distinct()
.collect(Collectors.toList());
Map<Long, CityDto> cityDtoMap = new LinkedHashMap<>();
List<CityDto> cityDtos = resultStream
.map(tuple -> {
CityDto cityDto = cityDtoMap.computeIfAbsent(tuple.get("c_id", Long.class),
id -> new CityDto(
tuple.get("c_id", Long.class),
tuple.get("c_name", String.class)
)
);
if (tuple.get("d_id") != null) {
DistrictDto districtDto = new DistrictDto(
tuple.get("d_id", Long.class),
tuple.get("d_name", String.class));
if (!cityDto.getDistrictDtoList().contains(districtDto))
cityDto.getDistrictDtoList().add(districtDto);
}
if (tuple.get("s_id") != null) {
ShopDto shopDto = new ShopDto(
tuple.get("s_id", Long.class),
tuple.get("s_name", String.class));
if (!cityDto.getShopDtoList().contains(shopDto))
cityDto.getShopDtoList().add(shopDto);
}
return cityDto;
})
.distinct()
.collect(Collectors.toList());
Итоги
Мы еще раз рассмотрели ResultTransformer для получения более сложной проекции. Кроме того, показана альтернатива — как обойтись вовсе без него.
Код примера есть на GitHub.