Привет, Хабр! Меня зовут Николай Пискунов, я ведущий разработчик в подразделении Big Data. В блоге beeline cloud мои коллеги уже знакомили вас с SOLID, заглядывали под капот Python. Я же расскажу о том, как с наименьшими затратами получить динамически изменяемый запрос к БД, используя Spring Data JPA и Hibernate.
Самый простой пример — реализация эндпоинта, который обрабатывает запросы фильтра интернет-магазина. Например, когда вы задаете характеристики, согласно которым возвращаются определенные товары.
Определяемся с задачами по уровням
Level 1
Что дано: фронт, с которого в бэк на Spring boot летят запросы, а бэк, в свою очередь, должен сделать соответствующую выборку в БД Postgresql и вернуть результат на фронт.
Если кто не помнит, Spring boot — это фреймворк на java, заточенный под веб. Хотя с его помощью решается множество других задач.
Предположим, что у нас есть база данных, а в ней одна простая таблица:
Из нее для фронта мы должны отдать записи, отфильтрованные по полю fooFieldOne. Для этого в Spring Data JPA реализован довольно приятный синтаксис:
public interface FooEntityRepository extends JpaRepository<FooEntity, UUID> {
List<FooEntity> findByFooFieldOne(String fooFieldOne);
}
При вызове метода findByFooFieldOne Hibernate конструирует запрос, эквивалентный нативному SQL-запросу:
SELECT * FROM foo WHERE foo_field_one = ‘blabla’;
В итоге мы получим массив элементов, которые удовлетворяют запросу, и после обработки мы возвращаем данные на фронт.
Если же потребуется фильтровать данные по двум полям, то Spring boot JPA также порадует. Ничего сложного:
public interface FooEntityRepository extends JpaRepository<FooEntity, UUID> {
List<FooEntity> findByFooFieldOne(String fooFieldOne);
List<FooEntity> findByFooFieldOneAndFooFieldTwo(String fooFieldOne, String fooFieldTwo);
}
Level 2
А теперь к тому, что было дано выше, мы добавляем дополнительную таблицу bar, которая связана с таблицей foo. Структура БД станет вот такой:
Теперь фронту понадобится фильтрация по полю bar_field_one. На этот случай Spring Data JPA предоставляет аннотацию @Query, с помощью которой мы сможем расширить функционал методов нашего репозитория следующим образом:
public interface FooEntityRepository extends JpaRepository<FooEntity, UUID> {
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE f.foo_field_one = ?1 and b.bar_field_one = ?2
""", nativeQuery = true)
List<FooEntity> findByFooFieldOne(String fooFieldOne, String barFieldOne);
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE f.foo_field_one = ?1 and f.foo_field_two = ?2 and b.bar_field_one = ?3
""", nativeQuery = true)
List<FooEntity> findByFooFieldOneAndFooFieldTwo(String fooFieldOne, String fooFieldTwo, String barFieldOne);
}
Как видим, достаточно знать SQL, чтобы получить нужный фронту результат.
Level 3
Но что, если нам нужно получать данные на основе динамически заполняемых полей? То есть с фронта нам может прийти запрос, где заполнены не все поля.
Конечно, мы можем проработать все варианты заполненных полей примерно следующим образом:
public interface FooEntityRepository extends JpaRepository<FooEntity, UUID> {
List<FooEntity> findByFooFieldOne(String fooFieldOne);
List<FooEntity> findByFooFieldTwo(String fooFieldTwo);
List<FooEntity> findByFooFieldOneAndFooFieldTwo(String fooFieldOne, String fooFieldTwo);
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE f.foo_field_one = ?1 and b.bar_field_one = ?2
""", nativeQuery = true)
List<FooEntity> findByFooFieldOneAndBarFieldOne(String fooFieldOne, String barFieldOne);
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE f.foo_field_two = ?1 and b.bar_field_one = ?2
""", nativeQuery = true)
List<FooEntity> findByFooFieldTwoAndBarFieldOne(String fooFieldTwo, String barFieldOne);
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE f.foo_field_one = ?1 and f.foo_field_two = ?2 and b.bar_field_one = ?3
""", nativeQuery = true)
List<FooEntity> findByFooFieldOneAndFooFieldTwoAndBarFieldOne(String fooFieldOne, String fooFieldTwo, String barFieldOne);
@Query(value = """
SELECT f.* FROM foo f
JOIN bar b on f.id = b.foo_id
WHERE b.bar_field_one = ?1
""", nativeQuery = true)
List<FooEntity> findByBarFieldOne(String barFieldOne);
}
Для этого в сервис-классе, который будет обрабатывать запрос, потребуется реализовать алгоритм выбора нужного метода. И, по сути, мы получаем требуемый результат.
Но что, если появятся другие динамически заполняемые поля для фильтрации? Например, их станет шесть, а это уже 21 вариант различных комбинаций. То есть наш репо-класс будет очень и очень большой. Вместе с тем вырастет и сложность сервис-класса. Поддерживать код станет просто невозможно.
Для решения этой проблемы предусмотрена аннотация @PersistenceContext. Это значит, что мы сможем вытащить из контекста Hibernate’овый EntityManager.
Предположим, что все поля фильтра из запроса складываются в dto:
@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class GetParams {
String fooFieldOne;
String fooFieldTwo;
String barFieldOne;
}
И, создав дополнительный репо-класс, нам нужно реализовать требуемый запрос к БД:
@Repository
public class FooEntityRepository {
@PersistenceContext
EntityManager entityManager;
public List<FooEntity> findFoos(GetParams params) {
Map<String, Object> parameterMap = new HashMap<>();
StringBuilder builder = new StringBuilder();
// WHERE 1 = 1 требуется на случай, если с фронта не будет получено никаких значений фильтра
StringBuilder whereBuilder = new StringBuilder("WHERE 1 = 1").append(System.lineSeparator())
// Первая строка селекта
builder
.append("SELECT f.* FROM foo f ")
.append(System.lineSeparator())
// Добавим условие «если было передано поле fooFieldOne»
if (StringUtils.isNotBlank(params.getFooFieldOne)) {
whereBuilder
.append("AND f.foo_field_one = :fooFieldOne")
.append(System.lineSeparator());
parameterMap.put("fooFieldOne", params.getFooFieldOne());
}
// Добавим условие «если было передано поле fooFieldTwo»
if (StringUtils.isNotBlank(params.getFooFieldTwo)) {
whereBuilder
.append("AND f.foo_field_two = :fooFieldTwo")
.append(System.lineSeparator());
parameterMap.put("fooFieldTwo", params.getFooFieldTwo());
}
// Приджойним таблицу bar и добавим условие «если было передано поле barFieldOne»
if (StringUtils.isNotBlank(params.getBarFieldOne)) {
builder
.append("JOIN bar b on f.id = b.foo_id")
.append(System.lineSeparator())
whereBuilder
.append("AND b.bar_field_one = :barFieldOne")
.append(System.lineSeparator());
parameterMap.put("barFieldOne", params.getBarFieldOne());
}
// Соберем и подготовим весь запрос
builder
.append(whereBuilder);
String nativeQuery = builder.toString();
Query query = entityManager.createNativeQuery(nativeQuery, RentCarCarEntity.class);
for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
nativeQuery = nativeQuery.replace(":" + entry.getKey(), entry.getValue().toString());
query.setParameter(entry.getKey(), parameterMap.get(entry.getKey()));
}
log.debug("\n{}\n", nativeQuery);
// Выполним и вернем результат
return query.getResultList();
}
В результате мы получаем динамически сформированный нативный запрос к БД, который вернет требуемый результат на фронт.
А если у нас появятся дополнительные поля для фильтрации или дополнительные таблицы и связки, и, тем более, потребуется использование SQL-операторов order, having или получать данные постранично, то лучше переписать этот класс, используя один из порождающих паттернов проектирования.
Ну а если количество запросов возрастет до 100 в секунду и выше, то лучше внедрять Redis и Elastic. Хотя в моей практике были случаи использования исключительно мощностей БД, которая обрабатывала подобные запросы при нагрузке в ~300 rps.
Другие статьи по разработке
Как развернуть CRM за 7 шагов? CRM помогает компании провести клиентов от знакомства до формирования лояльности, не теряя по пути информацию и автоматизируя часть процесса. Рассказываем, как за 7 шагов внедрить CRM-платформу так, чтобы сотрудники не перестали ей пользоваться.
Почему middle- и senior-разработчикам нужно участвовать в хакатонах? Если вы считаете, что хакатоны — конкурсы только для начинающих специалистов, пришло время изменить мнение. Рассказываем, какую пользу участие в хакатонах может принести продвинутым разработчикам. Создать что-то новое, приобрести полезные контакты, получить признание — только небольшая часть аргументов.
Как Jmix помогает создавать бизнес-приложения? Jmix — открытый фреймворк, в основе которого лежат Java и Spring Boot. Он помогает разрабатывать высокопроизводительные приложения, работать с данными и проектировать интерфейс. В материале рассказываем о том, почему стоит выбрать фреймворк, какие преимущества он дает при работе с клиентами и как его применять на практике.
beeline cloud — secure cloud provider. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.