Как стать автором
Обновить
4
0
Артём Гордиенко @arvgord

Пользователь

Отправить сообщение

Соглашусь с автором что для каждой конкретной цели есть свой инструмент. Единственный момент - возможно вы упустили или не указали в статье, что у Hibernate также есть возможность проверки кода во время компиляции. Для этого нужно использовать Criteria API и библиотеку генерации метамоделей классов hibernate-jpamodelgen.

Приведу примеры кода с использованием метамодели и Criteria API.

Репозиторий:

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

public interface SpeakerRepository extends JpaRepository<Speaker, Long>, JpaSpecificationExecutor<Speaker> {
}

Сервис с использованием метамодели Speaker:

import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class SpeakerService {

    private final SpeakerRepository speakerRepository;

    public List<Speaker> findByLastName(String lastName) {
        return speakerRepository.findAll(hasLastName(lastName));
    }

    private Specification<Speaker> hasLastName(String lastName) {
        // Используется сгенерированная метамодель Speaker_.java
        return (root, query, cb) -> cb.equal(root.get(Speaker_.lastName), lastName);
    }
}

Метамодель Speaker_.java генерируется во время компиляции и её можно использовать для проверки корректности составленных запросов (типы данных, наименование полей).

Init не подходит в случаях когда для промежуточного состояния используется тот же самый объект.

Если для создания объекта необходимо передавать класс Cat - то такая возможность есть. Валидация в init также будет работать.

constructor(cat: Cat) : this(
  name = cat.name,
  age = cat.age
)

В сумме разница с Java не принципиальная получается.

По мне так разница большая получается, в Kotlin весь необходимый функционал встроен в возможности языка. Каждый раз в Java применять шаблон Builder для обычного POJO - такое себе.

Я вижу как минимум два варианта.

Использовать блок инициализации init, который будет вызываться при создании объекта и проверять свойства:

data class Cat(
    val name: String,
    val age: Int = 0
) {
  init {
        require(name.isNotBlank()) { "Name cannot be null or empty." }
        require(age in 0..30) { "Age must be between 0 and 30." }
    }
}

Второй вариант - использовать готовую библиотеку валидации Hibernate Validator:

data class Cat(
    @field:NotBlank(message = "Name cannot be null or empty.")
    val name: String,
    @field:Range(min = 0, max = 30, message = "Age must be between {min} and {max}.")
    val age: Int = 0
)

Раньше писал на java и теперь рад что перешёл kotlin, т.к. больше не приходится писать такой boilerpalte код. Вероятность ошибок снижается и код становится более читаемым.

Вот аналог вышеописанного Builder (исключая проверки) для класса Cat на Kotlin:

data class Cat(
    val name: String,
    val color: String = "Grey",
    val age: Int = 0,
    val weight: Double = 0.0,
    val isFluffy: Boolean = false,
    val meowVolume: Int = 1,
    val favoriteFoods: List<String> = emptyList()
)

И вызовы этого класса разными параметрами:

val cat1 = Cat("Vasiliy")
val cat2 = Cat(name = "Barsik", age = 2, favoriteFoods = listOf("Fish", "Chicken"))

Имя является обязательным параметром конструктора, остальные параметры если не будут указаны, то будет использовано значение по умолчанию. Также toString переопределяется автоматически т.к. это data класс. Мне кажется, что преимущество очевидно.

Использование StringBuilder для построения динамических SQL запросов больше похоже на антипаттерн, потому что:

  1. Это сложно поддерживать, читать и отлаживать. Если у вас изменится какое-либо поле или название параметра - все эти изменения необходимо будет отслеживать и править вручную. И если вы это упустите, то заметите в лучшем случае, только во время интеграционных тестов (если они есть =)) или что хуже - это выстрелит только на проде. При использовании того же Criteria API с Hibernate Static Metamodel Generator, все ошибки вы увидите уже во время компиляции.

  2. Отсутствие проверки типов данных. Можно сказать что тоже относится к первому пункту.

  3. Когда вы собираете строки вручную есть потенциальная вероятность SQL инъекций.

Если вам не нравится Criteria API и вы хотите работать со строками, вы можете использовать тот же JOOQ. На самом же деле есть множество готовых решений для построения динамических SQL запросов: Criteria API, Querydsl, jOOQ, MyBatis и т.д.

В указанном примере с Entity graph проблема декартова произведения не
ушла, а только переместилась в запрос getAllByIdIn. Из‑за этого и
OutOfMemoryError.

Об этом описано в посте:

Проблема декартова произведения решается частично, JOIN все-таки остаются в запросе.

И причина:

Если посмотреть, какая часть запроса привела к OutOfMemoryError (так
сказать, «взорвалась»), то это именно та часть, где происходят JOIN c
помощью @EntityGraph

По поводу:

Есть более эффективное решение с Entity graph/Join fetch

В большинстве случаев этот подход эффективнее, чем использование @BatchSize

Я сам думал что BatchSize не очень эффективен и часто встречал такое мнение в туториалах. Но когда я протестировал его в примерах из поста, он оказался быстрее всего. Возможно есть что-то более эффективное - я не спорю. Всё зависит от вашей задачи. По моему мнению для пагинации он достаточно эффективен и прост. Я вызываю всего-лишь одну строку findAll и получаю необходимый результат.

На самом деле проблема более общая - декартово произведение и преобразование реляционной модели данных в объектную. Как раз таки Hibernate и позволяет ёё решить в отличии от нативных запросов.

  1. По архитектуре может не быть возможности хранить бизнес-логику в БД - по многим причинам.

  2. Даже если использовать VIEW, без расчётов - вы просто перенесёте JOINы из вашего приложения в данный VIEW. Что также не решит проблему. В примере из поста (рассматриваю крайний случай), необходимо получить данные из двадцати зависимых таблиц. В каждой из таблиц есть две записи для одного клиента. В результате SQL запроса для получения одного клиента с зависимыми данными вы получите декартово произведение - миллион строк, которые необходимо передать по сети, переварить в приложении и преобразовать в один объект!

    Если же вы будете использовать BatchSize в результате SQL запросов вы получите 41 строку которая преобразуется в один объект, с меньшей нагрузкой на БД приложение и сеть.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность

Специализация

Backend Developer