Как построить REST API в Spring, возвращающий список сущностей с двумя полями без использования проекций Spring Data
Что проверяют
Понимаете ли вы, что отдавать сущность наружу плохо, и умеете ли достать из базы только нужные колонки средствами JPA и JDBC, без интерфейсных проекций Spring Data. Следом спрашивают про пагинацию и чем плох вариант «загрузить всё и смапить в сервисе».
Как отвечать
Контроллер во всех вариантах одинаковый: @RestController, метод с @GetMapping, который возвращает List<UserShort>, где UserShort это record с двумя полями. Разница в том, как получить данные:
- конструктор в JPQL:
select new com.example.api.UserShort(u.id, u.name) from User uчерезEntityManager.createQuery(jpql, UserShort.class). Это механизм самого JPA, в SQL уйдут только две колонки, аrecordподходит, потому что у него есть нужный конструктор. Тот же запрос в@Queryрепозитория интервьюер может засчитать как проекцию, лучше уточнить; - Criteria API:
cb.construct(UserShort.class, root.get("id"), root.get("name")), если запрос собирается динамически; JdbcClient(Spring Framework 6.1 и новее):jdbcClient.sql("select id, name from users").query(UserShort.class).list(). Без ORM и контекста персистентности, быстрее всего на больших выборках;JdbcTemplateсRowMapperили jOOQ, если они уже есть в проекте.
Вариант с findAll() и map в сервисе рабочий, но он читает все колонки, создаёт управляемые сущности и может потянуть ленивые связи. Для двух полей это лишняя работа, упомяните его как антипример.
Зрелый ответ добавляет детали: список без пагинации опасен, лучше принимать Pageable или limit и offset. Сервис стоит пометить @Transactional(readOnly = true). Наружу уходит DTO, а не сущность: так не утекут лишние поля и не случится LazyInitializationException при сериализации.
Где спотыкаются
Отдают сущность и прячут поля через @JsonIgnore. Из базы всё равно читается вся строка, а при двусторонних связях легко получить бесконечную рекурсию в JSON.
Пишут в select new короткое имя класса. JPQL ждёт полное имя с пакетом, иначе Hibernate не найдёт класс и запрос не соберётся.
Отказываются от сущностей, но тут же достают связь в цикле через u.getDepartment().getName() при маппинге и получают проблему N+1.