JavaRush /Курсы /Spring Data JPA /Legacy-паттерны в репозиториях

Legacy-паттерны в репозиториях

Spring Data JPA
16 уровень , 0 лекция
Открыта

1. Откуда берётся legacy в репозиториях

Если вы когда-нибудь гуглили «Spring Data JPA repository example», вы знаете, как это обычно происходит: открываешь статью, видишь знакомые аннотации, копируешь кусок кода — и через 15 минут у тебя есть работающий метод. Проблема в том, что часть таких статей писалась под Spring Data JPA пятилетней (а то и десятилетней) давности. Код может компилироваться, но стиль и семантика уже не соответствуют современному API, особенно если у нас baseline Spring Boot 4 + Spring Data JPA 4.x.

Здесь важно поймать одну мысль: legacy — это не ругательство, а метка времени. Как старая фотография с кнопочным телефоном: на момент снимка он был нормальный, но сегодня странно планировать архитектуру приложения «как в 2012». В Spring Data это особенно заметно, потому что менялись имена методов, добавлялись новые возвращаемые типы, прояснялась семантика ссылочных объектов и появлялись более аккуратные способы описывать чтение данных.

К этому месту derived queries, JPQL, projections и native queries уже знакомы сами по себе. Боль обычно начинается не из-за нехватки инструментов, а из-за обратного: их много, и репозиторий быстро превращается в свалку, если смешивать их без логики. Поэтому сначала нужен не ещё один синтаксис, а нормальный радар для smell-ов.

Отсюда рождается первая большая причина legacy-репозиториев: «код из интернета» часто написан под другую версию и под другой уровень зрелости команды. Учебный пример мог сознательно упрощать или «смешивать всё в одном месте», чтобы показать 5 фич за 10 минут. Но когда такой пример становится вашим основным репозиторием в проекте — он начинает жить своей жизнью и расползаться.

Вторая причина проще и грустнее: репозиторий — это место, куда удобно «скинуть ещё один метод». Это почти как папка Downloads в операционной системе: сначала там лежит один файл, потом двадцать, а потом вы уже боитесь туда заходить. Репозиторий становится свалкой случайных методов не потому, что разработчик ленивый, а потому что у репозитория редко есть «владелец API», который следит за формой и дисциплиной.

Сейчас нам как раз и нужен такой радар: научиться распознавать репозитории, которые формально работают, но уже тянут проект в legacy, и отделять их от API, где по интерфейсу видно намерение.

2. Legacy-style repository как API

Репозиторий в Spring Data JPA — это не просто «интерфейс, который умеет ходить в базу». Это публичный контракт вашего data-layer. Сервисный слой (и иногда query-слой) будет вызывать методы репозитория так же, как вы вызываете методы стандартной библиотеки: ожидая, что они предсказуемы, читаемы и не устроят сюрпризов.

legacy-style repository — это репозиторий, который формально работает, но выглядит так, будто его писали «по дороге» и «по ситуации». Обычно у него нет единой логики по двум вещам: как выбирать механизм запросов и как выбирать возвращаемые типы. В итоге в одном интерфейсе рядом оказываются derived queries, JPQL, native SQL, pagination, projections и даже странные методы “на всякий случай”, при этом по именам и сигнатурам сложно понять, что из этого «основная дорога», а что «одноразовый эксперимент».

У такого репозитория есть неприятный эффект: он не ломается сразу. Он ломается в момент, когда вы начинаете его развивать. Команда открывает интерфейс, видит «тут уже можно всё», и добавляет ещё пару методов в том стиле, который ей привычнее. Через неделю репозиторий становится смесью трёх разных вкусов кофе (и все — растворимые), а через месяц никто не помнит, почему один список возвращается как List<Product>, второй как Page<Product>, третий как List<ProductCatalogRow>, а четвёртый вообще как List<Object[]> из какой-то древней статьи. И самое обидное: большинство проблем здесь не про SQL, а про дизайн API.

Чтобы «пощупать» проблему, представьте, что репозиторий — это меню в кафе. Современное меню читается быстро: разделы, понятные названия блюд, предсказуемые порции. Legacy-меню — это лист А4, где между борщом и десертами внезапно вставлена инструкция «как поменять дворники на машине», потому что «ну раз сюда уже что-то пишем». Вроде и полезно, но как-то тревожно.

Дальше разберём главные симптомы legacy-репозитория. Они часто идут набором, как «комбо-обед».

3. Симптом №1: giant repository

Сначала это выглядит даже удобно. У вас есть ProductRepository, и туда же вы добавили метод «найти по статусу», «найти дорогие», «прочитать всё SQL-ом», «поиск по имени с пагинацией». Всё в одном месте: красота. Но если в интерфейсе нет логики, почему методы именно такие, он начинает напоминать кухонный ящик, где лежат ложки, отвертка, батарейки, два болта и чей-то непонятный ключ. В момент, когда что-то нужно срочно найти, вы достаёте всё подряд. С репозиториями происходит то же самое.

Посмотрим на типичный пример «вроде норм, но уже тревожно»:

import java.math.BigDecimal;
import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Derived query: читается просто, но это только один из «языков» в этом интерфейсе
    List<Product> findByStatus(ProductStatus status);

    // JPQL: уже другой механизм запросов (и другой стиль чтения)
    @Query("""
           select p
           from Product p
           where p.price > :min
           """)
    List<Product> findExpensive(@Param("min") BigDecimal min);

    // Native SQL: третий механизм в одном и том же репозитории
    // Важно: пример специально «плохой» по стилю (select *), чтобы показать симптом
    @Query(value = """
                   select *
                   from product
                   """, nativeQuery = true)
    List<Product> readEverythingRaw();

    // Derived query + pagination: ещё одна вариация контракта (Page + Pageable)
    Page<Product> findByNameContainingIgnoreCase(String text, Pageable pageable);
}

Формально всё ок. Но это уже пример интерфейса, где четыре метода используют три разных «языка чтения»: derived query, JPQL и native SQL. Вопрос не в том, что так нельзя. Вопрос в том, что через месяц вы откроете этот интерфейс и поймёте: «А какой стиль здесь основной?». Если ответ «любой, который быстрее написать», вы уже на тропинке к giant repository.

Как обычно выглядит более здоровый фрагмент? Он не обязан быть идеальным и точно не претендует на весь интерфейс целиком. Его цель проще: чтобы по сигнатурам было видно намерение и чтобы интерфейс не провоцировал случайное разрастание. Минимальный здоровый шаг — держать методы в читаемой форме и в тех возвращаемых типах, которые прямо выражают смысл.

import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Один бизнес-ключ -> один результат (и он может отсутствовать)
    Optional<Product> findBySku(String sku);

    // Отдельный контракт на подсчёт, без лишней нагрузки сущностями
    long countByStatus(ProductStatus status);

    // Список + явная сортировка (контракт «как упорядочить» не прячется)
    List<Product> findByStatus(ProductStatus status, Sort sort);
}

Здесь нет магии. Но есть дисциплина: один метод ищет ровно один товар по бизнес-ключу (и честно говорит, что его может не быть), второй метод считает, третий возвращает список с явной сортировкой. Такой интерфейс вызывает меньше вопросов и лучше защищён от «случайного расширения».

Чтобы связать это с нашим проектом shop-data-jpa, полезно помнить архитектурное решение: package-by-feature. Это уже антидот от giant repository на уровне структуры. У нас есть catalog, inventory, ordering, и это не просто красивые папки. Это подсказка: если вы вдруг хотите положить в ProductRepository метод “найти заказы по email” — скорее всего, вы просто промахнулись пакетом.

Иногда giant repository появляется даже внутри одной фичи, когда репозиторий пытается одновременно быть и «CRUD-репозиторием», и «отчётным центром», и «админским поиском», и «местом для экспериментов с native SQL». На этом этапе помогает простое правило: если метод читается как «отдельная подсистема», его лучше оформить отдельной read-моделью (projection/record) и дать ему нормальное имя, а не прятать смысл в хаосе.

4. Симптом №2: entity возвращают везде

Очень распространённая интернет-привычка выглядит так: «репозиторий — это про сущности, значит он должен возвращать сущности». И дальше всё, даже каталог для админки, даже список для выпадайки на UI, даже сводка заказа — всё возвращает полные entity. Кажется логичным… пока use case не просит всего 2–3 поля.

В этот момент возвращать entity “по умолчанию” становится не универсальностью, а ленивым default. Репозиторий перестаёт различать write-модель и лёгкое чтение, а вызывающий код получает полную persistence-модель там, где нужна короткая read-модель.

Не потому что entity плохая, а потому что это удобный способ вообще не думать о форме результата.

Представим два сценария в catalog:

1) Нам нужен товар для изменения цены (то есть мы реально работаем с write-моделью). Тогда entity — норм.

2) Нам нужен список товаров в каталоге: название + цена. Грузить всё остальное (статус, связи, детали, и т.д.) — часто бессмысленно. Здесь логичнее projection.

Вот самый простой способ показать «современный поворот» на уровне одного метода:

import java.math.BigDecimal;

// Projection: контракт «читаем только нужные поля», а не «тащим всю entity»
public interface ProductCatalogRow {
    String getName();
    BigDecimal getPrice();
}

А теперь метод репозитория, который возвращает не entity, а read-модель:

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Важно: возвращаем не Product, а read-модель для каталога (частичное чтение)
    List<ProductCatalogRow> findByStatus(ProductStatus status);
}

Заметьте, метод всё ещё derived — и это нормально. Но по сигнатуре стало видно, что это именно «каталожное чтение», а не загрузка всей сущности ради будущего изменения. Это и есть один из признаков современного стиля: вы выражаете намерение не только именем, но и формой результата.

Если хочется быстро проверить себя, можно задавать вопрос почти как на собеседовании, но самому себе: «Если я сейчас верну entity, я понимаю, зачем мне все её поля и связи?». Если ответ «ну, пусть будет» — это тот самый момент, когда репозиторий начинает превращаться в legacy-слой.

5. Симптом №3: method-name monster

Derived queries — отличный инструмент. Но у него есть коварная сторона: он даёт ощущение, что можно «дописать ещё чуть-чуть» в имя метода. А потом ещё. А потом ещё. И вот уже у вас метод, который не помещается на экран, и чтение которого напоминает расшифровку древнего заклинания.

Вот типичный «монстр», который часто появляется, когда у нас много условий:

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository {

    // Имя метода начинает кодировать весь запрос, а сигнатура перестаёт читаться как API
    List<Product> findByStatusAndCategoryIdAndPriceBetweenAndNameContainingIgnoreCaseOrderById(
            ProductStatus status, // фильтр по статусу
            Long categoryId,       // фильтр по категории
            BigDecimal min,        // нижняя граница цены
            BigDecimal max,        // верхняя граница цены
            String text            // поиск по имени
    );
}

Технически Spring Data это разберёт. Но как API это уже плохой контракт. Он нарушает главное правило читаемости: сигнатура перестаёт быть «быстрым описанием намерения» и превращается в «встроенный язык запросов прямо в Java-именах». Такой код тяжело обсуждать в команде и тяжело менять. Любое новое условие превращает имя в ещё более длинную конструкцию, а метод начинает выглядеть как чат-бот, который не может остановиться.

Один из признаков современного стиля — умение остановиться. Если имя перестало читаться, лучше честно перейти к явному запросу. Даже если вы чуть-чуть больше печатаете, вы выигрываете в ясности.

import java.math.BigDecimal;
import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository {

    // Явный запрос вместо «романа» в имени метода:
    // имя выражает intent, а детали фильтра живут в JPQL
    @Query("""
           select p
           from Product p
           where p.status = :status
             and p.category.id = :categoryId
             and p.price between :min and :max
             and lower(p.name) like lower(concat('%', :text, '%'))
           order by p.id
           """)
    List<Product> findCatalogChunk(@Param("status") ProductStatus status,
                                   @Param("categoryId") Long categoryId,
                                   @Param("min") BigDecimal min,
                                   @Param("max") BigDecimal max,
                                   @Param("text") String text);
}

Смысл здесь не в том, что JPQL «лучше». Смысл в том, что API снова стал человеческим. Имя findCatalogChunk понятно: «прочитать кусок каталога». А детали фильтра живут там, где им и место — в тексте запроса.

И обратите внимание: мы ещё не выбираем «универсальное правило выбора инструмента» — это будет следующая часть дня. Сейчас мы просто фиксируем симптом: если derived-имя стало монстром, это очень часто признак legacy-стиля, который растёт без контроля.

6. Симптом №4: призраки старого API

Один из самых простых способов отличить современный код от интернет-legacy — посмотреть на имена методов. У Spring Data JPA есть методы, которые в старых версиях существовали, потом были deprecated, а потом заменены более ясными.

Вы можете встретить в статьях (и, что хуже, в чужих проектах) примерно такие вызовы:

// В старых примерах так часто делали (и это сигнал «код из другой эпохи»)
Product p = productRepository.getOne(productId);   // legacy-стиль (старые версии)

Почему это тревожный сигнал? Потому что это обычно означает не только «метод устарел», но и «автор примера живёт в другой ментальной модели репозитория». Часто вместе с такими вызовами приходят и другие привычки: возвращение entity везде, native query “потому что так проще”, и репозиторий на 200 методов.

Современный стиль старается называть вещи так, чтобы по имени было видно намерение. Поэтому вместо исторического getOne в актуальном API закрепилось более честное getReferenceById (мы ещё подробно разберём семантику позже по плану дня). Смысл нам сейчас важен только один: не тащить в новый код старые имена без понимания.

Полезно держать в голове простую табличку «что встретишь в старых видео и как это принято писать сейчас» (не как догму, а как ориентир):

В старых примерах В современном стиле Почему это важно
getOne(id) getReferenceById(id) Яснее, что это именно “reference”
getById(id) getReferenceById(id) Убираем двусмысленность «загрузили или ссылку дали?»
методы с get… без контекста find…,
count…
,
exists…
Имя должно выражать контракт

Ещё раз: это не «религия имён». Это способ сделать репозиторий предсказуемым и защищённым от копипасты.

7. Симптом №5: native query по умолчанию

Native query — полезный инструмент. Мы в курсе специально выделяли ему место: отчётные запросы, vendor-specific штуки, иногда сложные выборки, которые неудобно выразить JPQL. Но именно поэтому native query должна быть как острый нож на кухне: пользоваться можно и нужно, но не пытаться им намазывать масло на хлеб.

Legacy-паттерн выглядит так: «JPQL не знаю / не люблю, поэтому всё делаю на SQL». И дальше в репозитории появляются методы вроде:

import java.util.List;

import org.springframework.data.jpa.repository.Query;

public interface ProductRepository {

    // Native SQL «по умолчанию»: пример симптома, а не рекомендация
    @Query(value = """
                   select * from product
                   """, nativeQuery = true)
    List<Product> readEverythingRaw(); // «ну а что, работает же»
}

Эта штука опасна не только потому, что select * — плохая привычка. Она опасна тем, что вы внезапно привязываете репозиторий к физической схеме: к именам таблиц, колонок, диалекту. Это может быть оправдано… но тогда вы должны понимать, зачем вы это делаете, и держать scope узким. В противном случае вы превращаете Spring Data JPA в «хранилище строк SQL», а JPA-слой становится просто декорацией.

Современный стиль в нашем курсе звучит прагматично: native query — это исключение, не дефолт. Если запрос можно выразить derived query, JPQL или projection — выбираем более высокий уровень. Если запрос реально отчётный или специфичный — ok, native. Но тогда мы пишем её дисциплинированно: понятное имя метода, явный результат (часто projection), аккуратный SQL, а не «прочитать всё подряд».

8. Признаки современного repository-стиля

После всех «симптомов» легко сорваться в другую крайность: «теперь всё надо писать только через один правильный инструмент». Это такая же ловушка. Здоровый репозиторий держится не на одном любимом API, а на трёх простых вопросах.

Во-первых, что именно делает метод. Имя должно выражать намерение, а не заставлять читателя гадать, зачем этот метод вообще существует.

Во-вторых, какую форму результата ждёт use case. Один объект, список, счётчик, лёгкая read-модель — это разные контракты. Если возвращаемый тип выбран “на всякий случай”, интерфейс быстро становится непредсказуемым.

В-третьих, какой самый простой механизм честно выражает этот запрос. Derived query хороша, пока имя читается. JPQL хороша, когда условие уже проще написать явно. Projection хороша, когда нужен не весь объект. Native query хороша там, где это действительно SQL-first сценарий, а не привычка. Если эти три ответа не читаются по интерфейсу, giant repository почти гарантирован.

Как только smell-ы стали видны, дальше уже можно выбирать инструмент по use case, а не по привычке или по памяти о случайной статье из интернета.

9. Типичные ошибки при работе с репозиториями

Ошибка №1: копирование кода из старой статьи без проверки версии и актуальности API.
Это происходит почти автоматически: вы видите getOne(), getById(), какие-то странные методы и думаете, что «раз автор написал — значит так правильно». В современном baseline (Spring Boot 4 / Spring Data JPA 4) такие примеры либо не компилируются, либо уводят вас в устаревшую семантику. Правильная привычка — хотя бы мельком сверяться с текущим API и помнить, что в интернете полно примеров из «другой эпохи».

Ошибка №2: складывать всё в один интерфейс “потому что так быстрее”.
Сначала это ускоряет разработку, потом превращается в долг: вы открываете репозиторий и не понимаете, что в нём базовый контракт, а что случайные хотелки разных задач. Репозиторий начинает расти как снежный ком, и любая доработка требует «пять минут найти, где тут что лежит». Современный стиль не запрещает разные механизмы запросов, но требует осмысленности и структуры.

Ошибка №3: возвращать entity из любого чтения по привычке.
Entity — отличная штука, когда вы реально работаете с доменной моделью внутри data-layer. Но как только use case просит короткую read-модель (каталог, summary, отчёт), entity-return everywhere начинает выглядеть как ленивый default. Плюс он подталкивает к тому, что сервисы начинают зависеть от лишних полей и связей. Projections существуют ровно для того, чтобы репозиторий мог быть точным в форме результата.

Ошибка №4: писать derived-имена “потому что можно”, даже когда они уже не читаются.
Derived queries хороши, пока имя метода — это читаемая фраза. Когда имя стало романом, API перестало быть API и стало «текстом запроса, закодированным в CamelCase». В этот момент лучше честно перейти на JPQL (@Query) или на более подходящий инструмент, чем продолжать растить монстра.

Ошибка №5: делать native query первым выбором, а не точечным исключением.
Native SQL иногда необходим. Но когда репозиторий превращается в набор строк select * from ..., вы теряете преимущества JPA-уровня, ухудшаете читаемость и увеличиваете связность со схемой. Современный стиль — это не запрет на native, а дисциплина: применять её там, где она действительно оправдана, и не подменять ею всё остальное.

1
Задача
Spring Data JPA, 16 уровень, 0 лекция
Недоступна
Чистый ProductRepository без giant-interface
Чистый ProductRepository без giant-interface
1
Задача
Spring Data JPA, 16 уровень, 0 лекция
Недоступна
Короткая read-модель вместо полной entity
Короткая read-модель вместо полной entity
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ