Здесь напишем более сложный ResultTransformer, чем в предыдущем примере. Помимо коллекции @OneToMany сущность будет содержать еще коллекцию @ManyToMany.
Кроме того, попробуем получить результат еще одним способом — без ResultTransformer, а с помощью операций над Stream.
Сущности и проекции
Итак, задача — получить проекцию с вложенными объектами. Сейчас у нас три сущности — City, District и Shop.
@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 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" + " 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<>(); @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" + " 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 .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.