Как получить проекцию OneToMany c помощью ResultTransformer (пример на Hibernate и Spring Boot)

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

Как видно выше, в мы задаем 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 c помощью ResultTransformer (пример на Hibernate и Spring Boot): 2 комментария

  1. А если в классе (помимо @OneToMany и @ManyToOne) присутствует также @ManyToMany, есть какие-то варианты работы с таким Entity-классом с помощью проекций, @SqlResultSetMapping или ResultTransformer? При этом данные для этого Entity-класса получаются из БД с помощью native-SQL в JPA-репозитории.

    1. Можно ResultTransformer написать, только он сложнее будет (не потому, что ManyToMany сложнее OneToMany), а потому что три вложенных объекта формировать сложнее, чем один (не один список будет, а два).
      Пример с ManyToMany тут. (с native query все так же будет).

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

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