Spring АйО
7.67K subscribers
262 photos
158 videos
339 links
Русскоязычное сообщество Spring-разработчиков.

Habr: bit.ly/433IK46
YouTube: bit.ly/4h3Ci0x
VK: bit.ly/4hF0OG8
Rutube: bit.ly/4b4UeX6
Яндекс Музыка: bit.ly/3EIizWy

Канал для общения: @spring_aio_chat
Download Telegram
🛡 Spring Tips: Service Layer Validation

Валидация данных – ключевой аспект любого приложения. В Spring она часто используется в параметрах методов @RestController, например:


@RestController
@RequestMapping("/api/products")
public class ProductController {

@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(@RequestParam @NotNull @Size(min = 3, max = 50) String name,
@RequestParam @NotNull @Min(0) @Max(10000) Double price) {
// Логика поиска продуктов
List<Product> products = productService.search(name, price);
return new ResponseEntity<>(products, HttpStatus.OK);
}
}


Валидация в сервисном слое

Валидация помогает гарантировать, что данные, поступающие в приложение, соответствуют требованиям. Для валидации в сервисном слое нужно добавить аннотацию @Validated над сервисом:


@Service
@Validated
public class EmailService {
public void send(@Email String email,
@Length(max = 10) String subject,
@NotBlank String body) {
// бизнес-логика
}
}


Теперь метод send вызовет ошибку, если данные не проходят валидацию:


emailService.send(
"i am not email",
"I am too loooooooooong",
""
);



jakarta.validation.ConstraintViolationException: send.body: must not be blank, send.email: must be a well-formed email address, send.subject: length must be between 0 and 10
...


Валидация DTO в методах сервиса

Чтобы не дублировать поля в разных методах, разработчики часто используют DTO. Аннотации валидации применимы и здесь:


public record EmailRequest(
@Email String email,
@Length(max = 10) String subject,
@NotBlank String body
) {
}


Но в этом случае помимо @Validated над классом, нужно также не забыть добавить @Valid перед типом параметра в методе:


@Service
@Validated
public class EmailService {
public void sendBatch(
@Valid List<EmailRequest> requests
) {
// do work
}
}


Следующий код вызовет ошибку:


emailService.sendBatch(
List.of(
new EmailRequest("not email", "test", "Hello"),
new EmailRequest("alex@spring.aio", "I am too loooooooooong", "")
)
);



jakarta.validation.ConstraintViolationException: sendBatch.requests[1].subject: length must be between 0 and 10, sendBatch.requests[1].body: must not be blank, sendBatch.requests[0].email: must be a well-formed email address


Валидация элементов коллекций

Кстати, точно также можно валидировать и элементы коллекций, а также ключи и значения в Map:


@Service
@Validated
public class EmailService {
public void search(
Map<@NotBlank String, @NotBlank String> searchParams
) {
// do work
}
}


Следующий код вызовет ошибку:


searchService.search(Map.of("", ""));



jakarta.validation.ConstraintViolationException: search.searchParams<K>[].<map key>: must not be blank, search.searchParams[].<map value>: must not be blank


#SpringBoot #SpringTips #Validation
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻 Spring Tips: Кастомные реализации репозиториев

Spring Data упрощает создание запросов, но стандартные методы не всегда подходят под конкретные задачи. В таких случаях мы можем создать собственные реализации методов репозиториев. Например, если мы хотим, чтобы запрос формировался динамически на основе фильтра и возвращал DTO с меньшим количеством полей, нам понадобится создать fragment interface и кастомный метод с собственной реализацией.


//Кастомный интерфейс
public interface CustomizedUserRepository {
List<UserDto> findAllUsers(UserFilter filter);
}

//Реализация нашего интерфейса, которая будет использоваться Spring'ом
public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
private final EntityManager em;

public CustomizedUserRepositoryImpl(JpaContext jpaContext) {
em = jpaContext.getEntityManagerByManagedType(User.class);
}

//Реализация нашего метода
@Override
public List<UserDto> findAllUsers(UserFilter filter) {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(UserDto.class);

var root = query.from(User.class);
var emailPath = root.<String>get(User_.EMAIL);
var usernamePath = root.<String>get(User_.USERNAME);
query.multiselect(root.get(User_.ID), emailPath, usernamePath);

var predicates = new ArrayList<Predicate>();

var email = filter.email();
if (StringUtils.hasLength(email)) {
predicates.add(cb.like(cb.lower(emailPath), "%" + email.toLowerCase() + "%"));
}

var username = filter.username();
if (StringUtils.hasLength(username)) {
predicates.add(cb.like(cb.lower(usernamePath), "%" + username.toLowerCase() + "%"));
}

query.where(predicates.toArray(new Predicate[]{}));
return em.createQuery(query).getResultList();
}
}

//Использование кастомного интерфейса
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {}


Теперь при инжекции UserRepository, кастомный метод будет доступен для вызова:


List<UserDto> users = userRepository.findAllUsers(new UserFilter("Maksim", null));


Важные замечания

1. CustomizedUserRepositoryImpl — полноценный Spring-бин, поддерживающий инжекцию других бинов и специфическую функциональность (AOT, Lifecycle Callbacks и т.д.).

2. Инжекция UserRepository может привести к циклической зависимости. Чтобы избежать этого, его можно получить через ApplicationContext.getBean().

3. Spring пытается автоматически обнаружить пользовательские fragment интерфейсы, если классы следуют соглашению об именовании с постфиксом Impl. Модифицировать значение по умолчанию можно через атрибут аннотации @EnableJpaRepositories:


@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")


4. Репозитории могут включать несколько пользовательских реализаций, которые имеют более высокий приоритет, чем базовая реализация, что позволяет переопределять базовые методы. Например, создадим кастомный интерфейс и переопределим метод save:


public interface CustomizedSave<T> {
<S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {
@Override
public <S extends T> S save(S entity) {
// наша кастомная реализация
}
}


Теперь, если мы объявим следующий репозиторий:


interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {}


то при вызове метода userRepository.save(user) будет использован метод из нашей реализации CustomizedSaveImpl.

5. В примерах выше мы рассматривали Spring Data JPA, но эта концепция поддерживается для всех модулей Spring Data (MongoDB, Redis, JDBC и т.д.).

Подробнее про реализацию кастомных репозиториев читайте в документации.

#SpringBoot #SpringTips #CustomRepository
Please open Telegram to view this post
VIEW IN TELEGRAM
Spring Tips: Переопределение и дополнение свойств для Spring Boot тестов

При разработке на Spring Boot часто возникает необходимость переопределять конфигурационные свойства для тестов. Представим, что у нас есть application.properties в директории src/main/resources со следующими свойствами:


spring.application.name=properties
spring.jpa.open-in-view=false


Мы хотим изменить spring.application.name и добавить новое свойство server.port в тестах. Интуитивно разработчики создают application.properties в src/test/resources, ожидая, что он дополнит основной файл. Однако это не так (подробное объяснение можно найти здесь). Рассмотрим несколько способов переопределения свойств в тестах.

1. @ActiveProfiles

Создайте файл application-test.properties в src/test/resources и активируйте его в тестах.


@SpringBootTest
@ActiveProfiles("test")
class ActiveProfileTest {

@Autowired
private Environment env;

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


2. Директория config

Spring загружает конфигурационные файлы в определенном порядке. Файлы в src/test/resources/config имеют приоритет и переопределяют свойства из src/main/resources. Более подробно ознакомиться с тем, по какому принципу Spring загружает свойства можно в документации.

3. @DynamicPropertySource

Используйте динамическую подмену свойств, если значения известны только во время выполнения.


@SpringBootTest
class DynamicPropertySourceTest {

@Autowired
private Environment env;

@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.application.name", () -> "new-name");
registry.add("server.port", () -> 8099);
}

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


4. @TestPropertySource с параметром locations

Укажите путь к *.properties файлу с нужными значениями.


@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class PropertiesSourceLocationTest {

@Autowired
private Environment env;

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


Продолжение в комментариях

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
🦥 Spring Tips: Аннотация @Lazy

По умолчанию Spring инициализирует все sigleton-бины во время запуска приложения. Аннотация @Lazy позволяет изменить это поведение, инициализируя бины только по мере необходимости. Используя аннотацию @Lazy можно существенно снизить потребление памяти и уменьшить время запуска приложения, отметив бины, которые используются в приложении не так часто и занимают немало памяти.

Способы применения

Чтобы сделать бин лениво инициализируемым, нужно отметить его аннотацией @Lazy в месте его объявления:


@Lazy
@Component
public class MyComponent {
//...
}

@Configuration
@Lazy
public class LazyConfig {
// Все объявленные в этом конфиге бины будут ленивыми
}

@Configuration
public class AppConfig {
//Этот конкретный бин будет ленивым
@Bean
@Lazy
public MyBean myBean() {
return new MyBean();
}
}


А также в месте его инжекции:


@Service
public class MyService {
private final MyBean myBean;
private final MyComponent myComponent;

public MyService(@Lazy MyBean myBean, @Lazy MyComponent myComponent) {
this.myBean = myBean;
this.myComponent = myComponent;
}
}


Если не воспользоваться аннотацией @Lazy в месте объявления бина или в месте его инжекции, то он НЕ БУДЕТ ленивым.

Ленивая инициализация контекста по умолчанию

Чтобы сделать инициализацию ленивой для всего контекста, можно добавить следущее свойство в конфигурационный файл:


spring.main.lazy-initialization=true


Однако стоит быть осторожным с этим подходом. Ошибки инициализации могут возникнуть не во время запуска приложения, а позже – в рантайме, что усложнит их обнаружение и исправление (что в целом справедливо и при ленивой инициализации лишь некоторых бинов). Но если вы всё таки решите использовать глобальную ленивую инициализацию, можно исключить определённые бины, инициализируя их жадно (во время запуска приложения) с помощью @Lazy(false):


@Service
@Lazy(false)
public class MyService {
// код
}


А много ли бинов лениво инициализируется на вашем проекте? Может быть вообще все? Поделитесь своим опытом в комментариях, будет интересно почитать!

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
🛡 Кастомные валидаторы в Spring Boot: Как сделать валидацию под себя

Валидация входных данных — важная часть любой системы, обеспечивающая целостность данных и предотвращение ошибок. Иногда стандартные аннотации Jakarta Bean Validation (например, @NotNull, @Size, @Pattern) не покрывают все наши потребности, и нам приходится писать свои собственные валидаторы. В этом посте мы разберем два подхода к созданию кастомных валидаторов в Spring Boot, а также узнаем, как настроить отображение ошибки валидации.

1. Кастомные валидации с мета-аннотациями

Если стандартных аннотаций не хватает, можно создать свою собственную аннотацию для валидации. Это удобно, если вам нужно сочетать несколько стандартных аннотаций в одной или переиспользовать уже имеющиеся аннотации. Например, давайте создадим аннотацию @CardNumber, которая проверяет, соответствует ли строка формату номера кредитной карты.


@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp = "([0-9]{4}-){3}[0-9]{4}$", message = "Invalid credit card number")
@Constraint(validatedBy = {})
public @interface CardNumber {
String message() default "Invalid credit card number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}


В этом примере аннотация @CardNumber использует @Pattern для проверки формата номера кредитной карты. Такой подход упрощает код, потому что вся логика валидации сконцентрирована в одной аннотации.


@PostMapping("/card")
void checkCardNumber(@RequestParam @Valid @CardNumber String cardNumber) {
}



@Test
public void validCardNumber() throws Exception {
mockMvc.perform(post("/card")
.param("cardNumber", "1111-1111-1111-1111"))
.andExpect(status().isOk())
.andDo(print());
}

@Test
public void invalidCardNumber() throws Exception {
mockMvc.perform(post("/card")
.param("cardNumber", "1111-1111-1111"))
.andExpect(status().is(400))
.andDo(print());
}


2. Реализация собственных валидаторов

Когда стандартные аннотации не подходят, можно создать собственные валидаторы. Для этого нужно реализовать интерфейс ConstraintValidator.

Предположим, нам нужно валидировать пароли с определенными требованиями. Для этого создадим аннотацию @ValidPassword и валидатор PasswordValidator.


@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface ValidPassword {
String message() default "Invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int minLength() default 8;
String specialChars() default "!@#$%^&*()";
}

public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
private int minLength;
private String specialChars;

@Override
public void initialize(ValidPassword constraintAnnotation) {
this.minLength = constraintAnnotation.minLength();
this.specialChars = constraintAnnotation.specialChars();
}

@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}

boolean hasUpperCase = !password.equals(password.toLowerCase());
boolean hasLowerCase = !password.equals(password.toUpperCase());
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecialChar = password.chars().anyMatch(ch -> specialChars.indexOf(ch) >= 0);
boolean isLongEnough = password.length() >= minLength;

return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar && isLongEnough;
}
}


В данном примере аннотация @ValidPassword проверяет, что пароль содержит и верхний, и нижний регистр, цифры, специальные символы и имеет достаточную длину. Логика валидации инкапсулирована в PasswordValidator.

Продолжение в комментариях 👇

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
Spring Tips: Переопределение свойств через переменные окружения

Часто в файле application.properties (или application.yml) объявляются свойства, значения которых содержат переменные вида ${ENV_PROPERTY_KEY:defaultValue}:


spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}/${POSTGRES_DB_NAME:local_dev_db}
spring.datasource.username=${POSTGRES_USERNAME:root}
spring.datasource.password=${POSTGRES_PASSWORD:root}
spring.datasource.driver-class-name=org.postgresql.Driver


Такой подход позволяет переопределять свойства при запуске приложения в разных средах (local, test, prod и т.д.).

Например, если мы решили запустить наше приложение через Docker Compose, предварительно собрав его в Docker образ, то передача значений для объявленных нами переменных окружения будет выглядеть следующим образом:


spring_test_app:
image: spring_test_app:latest
build:
context: .
dockerfile: docker/Dockerfile
args:
DOCKER_BUILDKIT: 1
restart: "no"

# задаём конкретные значения тем самым переменным окружения
environment:
POSTGRES_HOST: postgres:5432
POSTGRES_DB_NAME: test_stand_db
POSTGRES_USERNAME: admin
POSTGRES_PASSWORD: admin
ports:
- "8080:8080"


В этом примере переменные из application.properties переопределяются через переменные окружения при запуске Docker контейнера.

Переопределение свойств напрямую

Но на самом деле, можно обойтись без дополнительных переменных в application.properties, переопределив Spring-свойства напрямую.

Теперь application.properties выглядит следующим образом:


spring.datasource.url=jdbc:postgresql://localhost:5432/local_dev_db
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.postgresql.Driver


А код сервиса нашего приложения в Docker Compose следующим образом:


spring_test_app:
image: spring_test_app:latest
build:
context: .
dockerfile: docker/Dockerfile
args:
DOCKER_BUILDKIT: 1
restart: "no"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres/test_stand_db
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: admin
ports:
- "8080:8080"


Для spring.datasource.url, spring.datasource.username и spring.datasource.password будут использованы те значения, которые мы указали в Docker Compose файле.

За счёт чего это становится возможным?

Spring Boot использует определенный порядок загрузки свойств приложения, чтобы обеспечить разумное переопределение значений. Переменные окружения загружаются позже и перезаписывают свойства, заданные в application.properties. Полный порядок загрузки можно найти в документации.

Relaxed Binding в Spring Boot

Кстати, обратите внимание на стиль написания названий свойств. Spring Boot использует концепцию Relaxed Binding, которая позволяет указать название свойства без полного совпадения. Например:


@ConfigurationProperties(prefix = "my.main-project.person")
class MyPersonProperties {
var firstName: String? = null
}


Для этого класса можно использовать следующие стили именования свойств:

- my.main-project.person.first-name — Kebab стиль для .properties/.yaml файлов
- my.main-project.person.firstName — CamelCase стиль
- my.main-project.person.first_name — Underscore стиль
- MY_MAINPROJECT_PERSON_FIRSTNAME — Uppercase + Underscore стиль для переменных окружения. При использовании Uppercase + Underscore стиля следует учитывать, что многие операционные системы ограничивают имена переменных окружения. Например, в Linux переменные могут содержать только буквы (a-z, A-Z), цифры (0-9) и символ подчеркивания (_). Подробнее читайте в документации.

В приведённом выше примере с Docker Compose файлом мы как раз воспользовались Uppercase + Underscore стилем, чтобы указать значения для нужных нам свойств. Именно такой вариант именования переменных окружения является негласным для Linux.

Ставь 🔥 если знал про это и 🤔 если слышишь впервые)

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
Spring Tips: Рекурсивные запросы в Hibernate

Рекурсивные запросы в SQL очень полезны при работе с иерархическими или графовыми структурами данных. Конструкция WITH, введенная в SQL:1999, позволяет задавать Common Table Expressions (CTE), которые представляют собой именованные подзапросы. CTE упрощают сложные запросы, улучшают их читаемость и, что самое важное, позволяют нам реализовать рекурсию.

Рассмотрим таблицу, которая хранит информацию о сотрудниках и их менеджерах:


CREATE TABLE employees (
employee_id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
employee_name VARCHAR(255),
manager_id INTEGER,
CONSTRAINT pk_employees PRIMARY KEY (employee_id)
);

ALTER TABLE employees ADD CONSTRAINT
FK_EMPLOYEES_ON_MANAGER FOREIGN KEY (manager_id)
REFERENCES employees (employee_id);


Эта таблица представляет собой список сотрудников, где у каждого сотрудника есть уникальный идентификатор (employee_id), имя (employee_name) и ссылка на менеджера (manager_id). Поле manager_id является внешним ключом, ссылающимся на employee_id в той же таблице, что позволяет создать иерархическую структуру, где один сотрудник может быть менеджером для других.

Предположим, что нам нужно выбрать всех подчиненных определенного менеджера. Для решения этой задачи мы можем использовать рекурсивный SQL запрос:


WITH RECURSIVE EmployeeHierarchy AS (
-- Базовый случай: начнем с верхнего уровня менеджера
SELECT employee_id, employee_name, manager_id
FROM employees
WHERE manager_id = :id

UNION ALL

-- Рекурсивный случай: найдем всех сотрудников, подчиненных найденным ранее сотрудникам
SELECT e.employee_id, e.employee_name, e.manager_id
FROM employees e
INNER JOIN EmployeeHierarchy eh ON e.manager_id = eh.employee_id
)

SELECT *
FROM EmployeeHierarchy;


До выхода версии Hibernate 6.2, для выполнения подобных запросов приходилось использовать nativeQuery. Пример с использованием entityManager выглядит следующим образом:


var sql = {SQL-запрос, представленный выше};

var employees = entityManager.createNativeQuery(sql, Employee.class)
.setParameter("id", 1)
.getResultList();


Либо так, если используется Spring Data JPA:


public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
@Query(value = "{SQL-запрос, представленный выше}", nativeQuery = true)
List<Employee> findAllEmployeesByManagerIdSQL(@Param("id") Integer id);
}

List<Employee> employees = employeeRepository.findAllEmployeesByManagerIdSQL(1);


С выходом Hibernate 6.2, появилась возможность писать рекурсивные запросы, используя HQL. Запрос, представленный выше, теперь можно написать так:


String jpql = """
WITH EmployeeHierarchy AS (
SELECT e.employeeId AS id, e.employeeName AS name, e.manager AS mgr
FROM Employee e
WHERE e.manager.id = :id

UNION ALL

SELECT e.employeeId, e.employeeName, e.manager.id
FROM Employee e
JOIN EmployeeHierarchy eh ON e.manager.id = eh.id
)
SELECT new Employee(
eh.id,
eh.name,
eh.mgr
)
FROM EmployeeHierarchy eh
""";

var employees = entityManager.createQuery(jpql, Employee.class)
.setParameter("id", 1)
.getResultList();


А также со Spring Data JPA:


public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
@Query(value = "{JPQL-запрос, представленный выше}")
List<Employee> findAllEmployeesByManagerIdJPQL(@Param("id") Integer id);
}

List<Employee> employees = employeeRepository.findAllEmployeesByManagerIdJPQL(1);


Как можно заметить, синтаксис HQL и SQL запросов довольно сильно похож. Но есть некоторые различия:

* В отличие от стандартного SQL, в Hibernate нет необходимости использовать ключевое слово RECURSIVE
* В Hibernate имена атрибутов CTE задаются через псевдонимы в выражении SELECT. Другими словами, в заголовке CTE имена не указываются.

#SpringTips #Hiberante #CTE
Please open Telegram to view this post
VIEW IN TELEGRAM
🐫 Underscore в имени Spring Data JPA методов

Все мы знаем, что для именования методов в java принято использовать lowerCamelCase. Но при определенных обстоятельствах правила можно и даже нужно нарушать! Например, когда в игру вступают фреймворки. Один такой пример мы сегодня с вами рассмотрим.

Предположим, у нас есть модель питомец-владелец питомца (многие-к-одному). Чтобы повысить перформанс нашего приложения, нам пришлось немного декомпозировать модель, добавив атрибут в класс питомца, в котором будет храниться информация о владельце – ownerLastName.

Класс питомца:

@Getter
@Setter
@Entity
@Table(name = "pet")
public class Pet {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id")
private Owner owner;
@Column(name = "owner_last_name")
private String ownerLastName;
}


И класс владельца питомца:

@Getter
@Setter
@Entity
@Table(name = "owner")
public class Owner {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@OneToMany(mappedBy = "owner", orphanRemoval = true)
private Set<Pet> pets = new LinkedHashSet<>();
}


Используя Spring Data JPA и derived методы нужно учитывать следующий нюанс. Получение списка питомцев по имени их владельца с выборкой именно по столбцу из таблицы с владельцами, а не по полю ownerLastName будет выглядеть следующим образом:

public interface PetRepository extends JpaRepository<Pet, Integer> {
List<Pet> findByOwner_LastName(String lastName);
}


Здесь нам пришлось правила именования и определить метод с "_" перед атрибутом. В этом случае Hibernate сгенерирует следующий запрос:

select *
from pet
left join owner on id=pet.owner_id
where owner.last_name=?


А если же мы хотим получить значения из таблицы pet, а не из owner, то нужно объявить метод без нижнего подчеркивания:

public interface PetRepository extends JpaRepository<Pet, Integer> {
List<Pet> findByOwnerLastName(String lastName);
}


Тогда поиск будет осуществлен по колонке из таблицы pet:

select *
from pet
where pet.owner_last_name=?


Подробнее про именование Spring Data JPA методов можно прочитать в документации.

P.S. А знаете ли вы ещё примеры подобного слома шаблонов?

#SpringTips #JPA
😡 Spring Data JPA: Проблема N+1 и один из способов её решения

Одной из часто обсуждаемых тем на собеседованиях по Spring Data JPA является проблема N+1 и возможные способы её решения. Многие отвечают, что её можно решить с помощью join fetch или @EntityGraph. Однако это не всегда так.

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


@Query("""
select distinct a
from Author a
left join fetch a.books
""")
List<Author> findAllFetch();

@EntityGraph(attributePaths = {"books"})
@Query("""
select distinct a
from Author a
""")
List<Author> findAllFetch();


Но что если нужно выбрать только часть данных, например, постранично? Тогда запросы будут выглядеть так:


@Query("""
select a
from Author a
left join fetch a.books
""")
Slice<Author> findAllFetch(Pageable page);

@EntityGraph(attributePaths = {"books"})
@Query("""
select distinct a
from Author a
""")
List<Author> findAllFetch(Pageable page);


Проблема в том, что когда мы используем Pageable, Hibernate сначала загрузит все записи в память, а затем отсортирует их. Это может привести к OutOfMemoryError и/или серьёзной деградации производительности.

В защиту Hibernate отметим, что столь неоптимизированное действие он не станет выполнять "втихую" и выведет следующий warning в логах:


WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!


Более того, указав следующее свойство в application.properties/yaml, Hibernate и вовсе начнёт падать с JpaSystemException:


spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true


Как же на самом деле стоит решать проблему N+1?

Правильное решение — использование аннотации @BatchSize:


@Entity
public class Owner extends Person {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
@BatchSize(size = 50)
private List<Pet> pets = new ArrayList<>();
}


Если коллекций много, можно настроить глобальный размер выборки в файле application.properties/yaml:


spring.jpa.properties.hibernate.default_batch_fetch_size=50


С использованием @BatchSize или глобального свойства запросы можно переписать следующим образом:


@Query("""
select a
from Author a
""")
Slice<Author> findAllFetch(Pageable page);

List<Author> findAll(Pageable pageable);


Вывод

Используйте @BatchSize или spring.jpa.properties.hibernate.default_batch_fetch_size, чтобы избежать проблемы N+1 без загрузки избыточного количества данных в память.

Стоит отметить, что используя этот подход важно помнить про необходимость обращения к элементам связанной коллекции в условиях наличия открытой транзакции, чтобы Hibernate смог загрузить необходимые данные. Несмотря на то, что spring.jpa.open-in-view=true позволяет нам быть уверенными в наличии открытой транзакции, он не является рекомендуемым.

Для более глубокого понимания оптимизации запросов рекомендуем ознакомиться с докладом экспертов сообщества Spring АйО – Ильи и Фёдора Сазоновых, с которым они выступали на последнем JPoint.

#SpringTips #JPA #DB
Please open Telegram to view this post
VIEW IN TELEGRAM
🌳 JPA Entity Graph и нюансы его использования

Entity Graph — это один из мощных инструментов JPA, который помогает разработчикам гибко управлять загрузкой связанных сущностей. Entity Graph позволяет динамически настраивать загрузку данных во время выполнения программы, что делает его особенно полезным в проектах со сложными структурами данных.

Команда Spring АйО подготовила статью, в которой рассмотрела, как использовать Entity Graph.

📚Читать на Хабр: https://habr.com/ru/companies/spring_aio/articles/844336/

#SpringTips #JPA
🎲 Рандомный порт для Spring Boot приложения

Чтобы запустить Spring Boot приложение на любом свободном порту достаточно указать в application.properties/yaml файле server.port=0. В этом случае Spring Boot автоматически подберёт свободный порт, и постарается избежать конфликта с другими приложениями.

Но что если хочется выбрать свободный порт из определенного диапазона? В таком случае можно использовать выражение ${random.int(min,max)}. Например, для выбора порта в пределах от 8000 до 8100 свойство будет выглядеть так:

server.port=${random.int(8000,8100)}

За случайный выбор значений отвечает класс RandomValuePropertySource.

#SpringTips #Simple