ResultTransformer для получения проекции ManyToMany (пример на Hibernate и Spring Boot)

Здесь напишем более сложный 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. Ту же логику придется прописать после получения списка (а лучше потока StreamTuple.

Итак, запрос такой же, но возвращается теперь 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *