Перейти к содержанию

Есть два основных подхода:

  • Через persistence.xml (классический для JPA).
  • Через hibernate.cfg.xml или Java-конфигурацию (при работе с «чистым» Hibernate).

hibernate.cfg.xml (Native Hibernate)

Если вы не используете JPA API, а подключаетесь напрямую к Hibernate, необходим hibernate.cfg.xml

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "https://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- JDBC -->
        <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
        <property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/mydb</property>
        <property name="hibernate.connection.username">myuser</property>
        <property name="hibernate.connection.password">mypassword</property>

        <!-- Dialect -->
        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>

        <!-- DDL -->
        <property name="hibernate.hbm2ddl.auto">update</property>

        <!-- Логирование -->
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>

        <!-- Кэш второго уровня -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <property name="hibernate.cache.region.factory_class">
            org.hibernate.cache.jcache.JCacheRegionFactory
        </property>
        <property name="hibernate.javax.cache.uri">ehcache.xml</property>

        <!-- Указываем пакеты/классы -->
        <mapping class="com.example.domain.User"/>
        <mapping class="com.example.domain.Order"/>
        <!-- Или сканируем пакет через аннотации: -->
        <!--<mapping package="com.example.domain"/>-->
    </session-factory>
</hibernate-configuration>

Hibernate-specific аннотации для оптимизации запросов

  • @Fetch

    • Пакет: org.hibernate.annotations.Fetch
    • Используется вместе с: @OneToMany, @ManyToMany, @OneToOne, @ManyToOne (хотя для ManyToOne/OneToOne чаще @Fetch(FetchMode.JOIN) не нужен).
    • Атрибуты:
      • value - FetchMode:
        • JOIN - явный JOIN в SQL при загрузке.
        • SELECT - традиционная «ленивая» выборка (может вызвать N+1).
        • SUBSELECT - Hibernate использует WHERE id IN (…) для пакетной загрузки.
    • Пример:
      @OneToMany(mappedBy = "order")
      @Fetch(FetchMode.SUBSELECT)
      private List<OrderItem> items;
      
    • При загрузке списка заказов Hibernate сначала выберет все orders, а потом разом подгрузит order_items через IN (…).
  • @BatchSize

    • Пакет: org.hibernate.annotations.BatchSize
    • Атрибуты:
      • size - int. Число записей, которые Hibernate будет загружать одним запросом при ленивой выборке коллекций или связанных сущностей.
    • Пример для коллекций:
      @OneToMany(mappedBy = "order")
      @BatchSize(size = 20)
      private List<OrderItem> items;
      
      • Если у вас есть 50 заказов и вы вызываете getItems() для каждого, Hibernate группирует их по 20, выполняя примерно 3 запроса вместо 50 (N+1).
    • Пример для сущностей:
      @Entity
      @BatchSize(size = 50)
      public class Product { … }
      
      • Когда лениво загружается Product через прокси, Hibernate сразу загрузит 50 записей по id IN (…) вместо одной.
  • @LazyToOne

    • Пакет: org.hibernate.annotations.LazyToOne и org.hibernate.annotations.LazyToOneOption
    • Назначение: позволяет сделать @OneToOne или @ManyToOne действительно ленивой без bytecode enhancement (с помощью прокси-обёртки).
    • Опции:
      • NO_PROXY - Hibernate создаёт прокси на связанный объект.
      • PROXY - аналогично NO_PROXY.
      • FALSE - эквивалент EAGER.
    • Пример:
      @OneToOne(fetch = FetchType.LAZY)
      @LazyToOne(LazyToOneOption.NO_PROXY)
      @JoinColumn(name = "profile_id")
      private UserProfile profile;
      
  • @FetchProfile

    • Пакет: org.hibernate.annotations.FetchProfile и org.hibernate.annotations.FetchMode
    • Назначение: позволяет заранее описать профили «fetch-plan» (какие связи JOIN FETCH выполнять) и активировать их в коде.
    • Атрибуты:
      • name - имя профиля.
      • fetchOverrides - массив @FetchProfile.FetchOverride, в котором указываются entity, association, mode (JOIN) и т.п.
    • Пример:
      @FetchProfile(
         name = "order-with-items",
         fetchOverrides = {
            @FetchProfile.FetchOverride(
               entity = Order.class,
               association = "items",
               mode = FetchMode.JOIN
            )
         }
      )
      @Entity
      public class Order { … }
      
    • В коде:
      session.enableFetchProfile("order-with-items");
      Order o = session.get(Order.class, id);
      // При загрузке Order Hibernate выполнит JOIN с order_items.
      
  • @Formula

    • Пакет: org.hibernate.annotations.Formula
    • Назначение: позволяет «привязать» вычисляемое поле: SQL-выражение, которое Hibernate вставляет в SELECT.
    • Атрибуты:
      • value - SQL-выражение, возвращающее одиночное значение.
    • Пример: ``` @Entity public class Order { @Id private Long id;

      // Вычисляемое поле «количество товаров»: @Formula("(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = id)") private int itemCount; } ``` - При выборке Order Hibernate подставит подзапрос и вернёт itemCount вместе с остальными колонками.

Hibernate-specific Аннотации классов

  • @org.hibernate.annotations.Cache Конфигурация L2-кэша конкретной сущности

    • usage @DynamicInsert / @DynamicUpdate Генерация SQL для INSERT/UPDATE только по непустым или изменившимся полям.
  • @SelectBeforeUpdate Перед выполнением UPDATE выполняет SELECT, чтобы понять, нужно ли вообще обновлять строку.

  • @OptimisticLocking Настройка оптимистической блокировки (VERSION или поля-считывателя).
  • @Subselect Маппинг сущности на подзапрос.
  • @Formula Виртуальное поле, выражение SQL, вычисляемое в запросе.
  • @FilterDef, @Filter Фильтры, активируемые на уровне сессии.

@Enumerated @OneToMany, @ManyToOne, @OneToOne, @ManyToMany orphanRemoval = true – автоматическое удаление "осиротевших" записей. @BatchSize (Hibernate) – загрузка коллекций пачками. @Fetch(FetchMode.SUBSELECT) – загрузка подзапросом вместо N+1. @OneToMany(mappedBy = "user") @Fetch(FetchMode.SUBSELECT) private List orders; @Embeddable + @Embedded @Embeddable @Inheritance – стратегии наследования (SINGLE_TABLE, JOINED, TABLE_PER_CLASS). @Entity @Inheritance(strategy = InheritanceType.JOINED)

Всегда используй LAZY там, где возможно (иначе N+1 проблема). Для @OneToMany по умолчанию LAZY, для @ManyToOne – EAGER (лучше явно указать LAZY).


Best Practices для JPA-сущностей

Иммутабельность и неизменяемость По возможности делай сущности неизменяемыми (immutable) через final поля и @Setter только где нужно. Используй DTO для передачи данных, а не сущности напрямую.

Оптимальные Fetch-стратегии Всегда LAZY, если не нужен EAGER. Используй @EntityGraph для динамической загрузки связей. JPQL/HQL с JOIN FETCH для явной загрузки.

@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id") User findByIdWithOrders(@Param("id") Long id);

Кэширование @Cacheable (Spring) + Hibernate L2 Cache (@Cache).

@Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Product { ... }

Оптимизация запросов Избегай SELECT * – используй @NamedEntityGraph или проекции (DTO, интерфейсы).

Для массовых операций используй @Modifying + @Query.

@Modifying @Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date") void deactivateInactiveUsers(@Param("date") LocalDate date);

Валидация и constraints @NotNull, @Size, @Email (из javax.validation).

@Column(nullable = false) @NotNull @Size(min = 3, max = 50) private String username;

Аудит (кто и когда изменил) @CreatedDate, @LastModifiedDate (Spring Data JPA).

@EntityListeners(AuditingEntityListener.class) public class User { @CreatedDate private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

}

Best Practices ✅ Всегда используй LAZY для связей (@ManyToOne, @OneToMany), если не нужен EAGER. ✅ Избегай N+1 через JOIN FETCH, @EntityGraph, @BatchSize. ✅ Валидация через jakarta.validation (@NotBlank, @Size). ✅ Кэширование (@Cacheable) для часто читаемых данных. ✅ Аудит (@CreatedDate, @LastModifiedDate) для отслеживания изменений. ✅ Оптимистичная блокировка (@Version) для конкурентных обновлений.


Аннотации класса

Стандартные JPA-аннотации

Аннотация Описание
@SecondaryTable Позволяет маппить сущность на несколько таблиц.
@Inheritance(strategy = InheritanceType.XXX) Стратегия наследования (SINGLE_TABLE, JOINED, TABLE_PER_CLASS).
@DiscriminatorColumn(name = "type") Используется с SINGLE_TABLE для хранения типа сущности.
@DiscriminatorValue("value") Указывает значение дискриминатора для текущего класса.
@Embeddable Объявляет класс как встраиваемый (не является сущностью).
@MappedSuperclass Класс-родитель, поля которого маппятся на таблицы наследников.

Hibernate-специфичные аннотации

Аннотация Описание
@DynamicInsert Генерирует SQL только для ненулевых полей.
@DynamicUpdate Обновляет только измененные поля (не весь объект).
@Immutable Запрещает изменения сущности (оптимизация для read-only).
@Proxy(lazy = false) Отключает ленивую загрузку прокси.
@Polymorphism(type = PolymorphismType.EXPLICIT) Указывает, включать ли класс в полиморфные запросы.
@Where(clause = "active = true") Фильтр для всех запросов к сущности.
@FilterDef(name = "activeFilter", parameters = @ParamDef(...)) Динамический фильтр.
@Filter(name = "activeFilter", condition = "active = :active") Применение фильтра.
@Subselect("SELECT ... FROM ...") Маппинг на SQL-представление (не таблицу).
@Synchronize({"table1", "table2"}) Указывает таблицы для проверки кэша.

Spring Data JPA и аудит

Аннотация Описание
@EntityListeners(AuditingEntityListener.class) Включает аудит (@CreatedDate, @LastModifiedBy).
@CreatedBy, @LastModifiedBy Автоматическое заполнение пользователя.
@CreatedDate, @LastModifiedDate Автоматическое заполнение дат.
@DomainEvents Публикация событий перед сохранением (Spring Data).
@Transactional Управление транзакциями (Spring).

Кэширование

Аннотация Описание
@Cacheable Включает кэширование (Spring).
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.XXX) Стратегия кэша (READ_ONLY, READ_WRITE, NONSTRICT_READ_WRITE).

Валидация (Jakarta Validation)

Аннотация Описание
@Validated Включает валидацию для класса (Spring).

Аннотации полей

Аннотация Описание
@EntityListeners Аудит (автоматическое заполнение createdAt/updatedAt).
@Enumerated(EnumType.STRING) сохранение enum как строки
@ManyToOne(fetch = LAZY) всегда ленивая загрузка
@OneToMany(orphanRemoval = true автоматическое удаление "осиротевших" записей
@BatchSize + @Fetch(FetchMode.SUBSELECT) борьба с N+1
@Transient поле не сохраняется в БД
@Version оптимистичная блокировка
@PrePersist, @PostLoad хуки жизненного цикла

JPA specification

Best Practice:

  • Всегда указывайте name, если имя таблицы ≠ имени класса
  • Используйте schema для многопользовательских БД
  • Добавляйте индексы на часто используемые поля
  • Уникальные constraints для бизнес-правил
    // Гарантирует, что email и phone уникальны:
    @Table(
        name = "users",
        uniqueConstraints = {
            @UniqueConstraint(name = "uk_user_email", columnNames = "email"),
            @UniqueConstraint(name = "uk_user_phone", columnNames = "phone")
        }
    )
    
  • Избегайте избыточности с @Column(unique = true)
    // Избыточно (дублирование):
    @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
    public class User {
        @Column(unique = true)  // Лучше оставить только это
        private String email;
    }
    
    // Лучше:
    public class User {
        @Column(unique = true)  // Проще и понятнее
        private String email;
    }
    

1. Аннотация @JoinColumn

Аннотация @JoinColumn используется для явно указания столбца-связки (foreign key) в таблице, когда вы описываете отношение между сущностями. Чаще всего применяется вместе с @ManyToOne, @OneToOne или в обратной (владельческой) стороне @OneToMany/@OneToOne.

1.1 Основное назначение

  • Указывает имя колонки в текущей таблице, которая хранит внешний ключ.
  • Позволяет детально сконфигурировать, какие именно свойства колонки (nullable, unique, insertable, updatable и т.д.) будут применены.
  • Используется на «владельческой» стороне отношения (та, что содержит внешний ключ).

1.2 Атрибуты @JoinColumn

Атрибут Тип Значение по умолчанию Описание
name String "" Имя столбца во «владельческой» таблице, содержащего FK. Если не указано, JPA сгенерирует имя по умолчанию: <имя_поля>_<имя_первичного_ключа> (например, customer_id).
referencedColumnName String "" Имя столбца в «целевой» таблице (та, на которую ссылаются). По умолчанию - первичный ключ целевой сущности.
unique boolean false Если true, для этого столбца будет добавлено ограничение UNIQUE.
nullable boolean true Определяет, может ли внешний ключ принимать значение NULL. Если nullable = false, будет NOT NULL.
insertable boolean true Если false, столбец не будет включён в SQL-операцию INSERT.
updatable boolean true Если false, столбец не будет включён в SQL-операцию UPDATE.
columnDefinition String "" Позволяет задать свой SQL-фрагмент для создания столбца (например, тип или дефолт).
table String "" Имя таблицы, в которой находится этот столбец; чаще используется при маппинге на несколько таблиц (@SecondaryTable).
foreignKey javax.persistence.ForeignKey @ForeignKey( value = ConstraintMode.PROVIDER_DEFAULT ) Позволяет задать дополнительные параметры внешнего ключа (имя ограничения, правило ON DELETE/ON UPDATE и т.д.).

Примечание. В более старых версиях JPA (до 2.1) атрибут foreignKey отсутствует.

1.3 Какие параметры принимает и что они означают

  1. name

  2. Тип: String

  3. Описание: конкретное имя колонки FK в текущей таблице.
  4. Пример:
    @JoinColumn(name = "customer_id")
    
  5. Если не указан: JPA сделает <имя_поля>_<pk_целевой_сущности>. Например, если поле называется customer и у сущности Customer PK - id, то столбец будет customer_id.

  6. referencedColumnName

  7. Тип: String

  8. Описание: явно указывает, на какой столбец целевой таблицы ссылается внешний ключ.
  9. Пример (если у Customer не id, а uuid в качестве PK):
    @JoinColumn(name = "customer_uuid", referencedColumnName = "uuid")
    
  10. Если не указан: автоматически берётся PK (обычно id).

  11. unique

  12. Тип: boolean

  13. Описание: если true, создаётся ограничение UNIQUE на уровне схемы. Используется редко, например, при @OneToOne, если хотим гарантировать «один к одному» на уровне БД.
  14. Пример:

    @JoinColumn(name = "profile_id", unique = true)
    

  15. nullable

  16. Тип: boolean

  17. Описание: если false, будет добавлено NOT NULL в DDL. По умолчанию true (разрешено NULL). Обычно для обязательных связей (optional = false) ставят nullable = false.
  18. Пример:

    @JoinColumn(name = "customer_id", nullable = false)
    

  19. insertable / updatable

  20. Тип: boolean

  21. Описание: управляют тем, включается ли колонка в SQL-запросы INSERT и/или UPDATE. Например, если хотите, чтобы колонка заполнялась только через БД (триггеры) или внешние механизмы, можно поставить insertable = false.
  22. Пример:
    @JoinColumn(name = "order_type_fk", insertable = false, updatable = false)
    
  23. Когда применяется: очень редко. Чаще встречается в сценариях «внешние» или «рассчитанные» FK.

  24. columnDefinition

  25. Тип: String

  26. Описание: дает возможность задать «сырой» SQL-фрагмент при создании столбца. Например, если нужен тип UUID или дефолтное значение.
  27. Пример:

    @JoinColumn(name = "customer_id", columnDefinition = "BIGINT DEFAULT 0")
    

  28. table

  29. Тип: String

  30. Описание: название таблицы, в которой находится этот столбец, если сущность маппится на несколько таблиц ( используются @SecondaryTable).
  31. Пример:

    @SecondaryTable(name = "customer_details")
    public class Customer {
        @Id
        private Long id;
        // ...
        @Column(table = "customer_details", name = "detail_info")
        private String detailInfo;
        // ...
        @OneToOne
        @JoinColumn(name = "passport_id", table = "customer_details")
        private Passport passport;
    }
    

  32. foreignKey (JPA 2.1+)

  33. Тип: javax.persistence.ForeignKey

  34. Описание: дополнительные настройки внешнего ключа: имя ограничения, поведение ON DELETE/ON UPDATE, режим ConstraintMode.NO_CONSTRAINT (чтобы не создавать, если внешний ключ моделируется вручную).
  35. Пример:
    @JoinColumn(
        name = "order_id",
        foreignKey = @ForeignKey(
            name = "FK_ORDER_CUSTOMER",
            value = ConstraintMode.CONSTRAINT
        )
    )
    private Order order;
    

1.4 Пример использования @JoinColumn

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Один заказ связан с одним пользователем (владелец связи - Order)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(
        name = "customer_id",           // имя колонки в таблице orders
        referencedColumnName = "id",    // ссылается на столбец id из таблицы customers
        nullable = false,               // NOT NULL
        foreignKey = @ForeignKey(        // явно задаём имя FK-constraint
            name = "FK_ORDER_CUSTOMER"
        )
    )
    private Customer customer;

    // остальные поля
}

---

## `@ManyToOne`

@Entity public class Comment { @Id @GeneratedValue private Long id;

@ManyToOne(fetch = FetchType.LAZY)  // Всегда LAZY для ManyToOne!
@JoinColumn(name = "author_id")     // Столбец в таблице Comment
private Author author;

}

`fetch = FetchType.LAZY` FetchType.LAZY (по умолчанию для @ManyToOne) или EAGER

`optional = false` Может ли связь быть null (по умолчанию true)

`cascade = CascadeType.ALL` Каскадные операции (сохранение/удаление)

`targetEntity = Author.class` Класс целевой сущности (если нужен дженерик)

---

@Entity public class Comment { @Id @GeneratedValue private Long id;

@ManyToOne(fetch = FetchType.LAZY)  // Всегда LAZY!
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_comment_user"))
private User author;

}

### Best Practices:

✅ Всегда используйте FetchType.LAZY
✅ Явно указывайте @JoinColumn с nullable = false для обязательной связи
✅ Добавляйте именованные foreign keys (@ForeignKey)
❌ Использование EAGER (риск N+1 проблемы)
❌ Отсутствие @JoinColumn (Hibernate сам создаст столбец с неочевидным именем)
✅ Всегда используйте LAZY (ленивую загрузку), если не нужен EAGER.
✅ Добавляйте @JoinColumn для явного указания имени столбца.
✅ Для обязательной связи указывайте optional = false.

## `@OneToMany`

@Entity public class Author { @Id @GeneratedValue private Long id;

@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();

}

`mappedBy = "author"`    Указывает поле в дочерней сущности, которое управляет связью

`cascade = CascadeType.ALL`    Каскадные операции

`orphanRemoval = true`    Удалять ли "осиротевшие" записи (при удалении из коллекции)

`fetch = FetchType.LAZY`    FetchType.LAZY (по умолчанию для @OneToMany) или EAGER

### Best Practices:

✅ Всегда инициализируйте коллекции (new ArrayList<>()).
✅ Используйте orphanRemoval = true, если нужно удалять дочерние записи.
✅ Избегайте EAGER (может привести к N+1 проблеме).

---

@Entity public class User { @Id @GeneratedValue private Long id;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();  // Инициализация обязательна!

}

### Best Practices:

✅ Всегда используйте mappedBy на стороне владельца
✅ Инициализируйте коллекцию (new ArrayList<>())
✅ Для удаления дочерних элементов используйте orphanRemoval = true

### Ошибки:

❌ Отсутствие mappedBy (приводит к созданию лишней join-таблицы)
❌ Использование EAGER (загрузит все дочерние записи сразу)

## `@ManyToMany`

@Entity public class Student { @Id @GeneratedValue private Long id;

@ManyToMany
@JoinTable(
    name = "student_course",
    joinColumns = @JoinColumn(name = "student_id"),
    inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();

}

@Entity public class Course { @Id @GeneratedValue private Long id;

@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();

}

`mappedBy = "courses"`    Указывает, какая сторона управляет связью

`cascade = CascadeType.PERSIST`    Каскадные операции

`fetch = FetchType.LAZY`    FetchType.LAZY (по умолчанию) или EAGER

`targetEntity = Course.class`    Класс целевой сущности

### Best Practices:

✅ Используйте Set вместо List (избегает дубликатов).
✅ Одна сторона должна быть владельцем (с @JoinTable).
✅ Избегайте EAGER (может загрузить всю БД).

---

@Entity public class Student { @Id @GeneratedValue private Long id;

@ManyToMany
@JoinTable(
    name = "student_course",
    joinColumns = @JoinColumn(name = "student_id"),
    inverseJoinColumns = @JoinColumn(name = "course_id"),
    foreignKey = @ForeignKey(name = "fk_student_course_student"),
    inverseForeignKey = @ForeignKey(name = "fk_student_course_course")
)
private Set<Course> courses = new HashSet<>();  // Set вместо List!

}

@Entity public class Course { @Id @GeneratedValue private Long id;

@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();

}

### Best Practices:

✅ Используйте Set вместо List для избежания дубликатов
✅ Одна сторона должна быть владельцем (с @JoinTable)
✅ Добавляйте именованные foreign keys

### Ошибки:

❌ Отсутствие mappedBy на одной из сторон (создаст две join-таблицы)
❌ Использование List без дополнительных мер против дубликатов

## Other INFO

Каскадные операции (cascade)
Определяют, какие операции применяются к связанным сущностям:

Тип каскада Описание
CascadeType.ALL - Все операции (сохранить, обновить, удалить)
CascadeType.PERSIST - Сохранить связанную сущность
CascadeType.MERGE - Обновить связанную сущность
CascadeType.REMOVE - Удалить связанную сущность
CascadeType.REFRESH - Обновить из БД
CascadeType.DETACH - Отсоединить от контекста

Правила каскадирования:

// Сохранит/обновит/удалит все заказы при сохранении пользователя @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) private List orders;


@OneToMany(mappedBy = "author", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) private List books;

Best Practices:
Для @ManyToOne обычно не используют каскады
Для @OneToMany часто используют CascadeType.ALL + orphanRemoval
Для @ManyToMany каскады используют осторожно
Опасные сценарии:
⚠️ CascadeType.ALL на @ManyToMany может случайно удалить связанные сущности
⚠️ Отсутствие orphanRemoval при удалении из коллекции оставляет "висячие" записи

# Java Bean Validation

Валидационные аннотации из Jakarta Bean Validation (JSR-380) могут применяться к разным объектам (Entity, DTO,
Form-объектам)

В Spring REST-контроллерах (DTO) - При получении HTTP-запроса Spring автоматически проверяет DTO перед вызовом метода
контроллера

- Клиент отправляет JSON в /users.
- Spring преобразует JSON в UserDto.
- До вызова метода createUser() запускается валидация.
- Если есть ошибки - возвращается 400 Bad Request с описанием ошибок.

---

`@NotBlank String name` Проверяет, что строка не null и не пустая (после обрезки пробелов)

`@NotEmpty List<String>` Проверяет, что строка/коллекция не null и не пустая

`@NotNull Long id` Проверяет, что поле не null

`@Size(min = 3, max = 50) String username` Проверяет длину строки/коллекции (min/max)

`@Pattern(regexp = "^[A-Za-z0-9]+$")` Проверяет соответствие регулярному выражению

`@Min(18) int age` Минимальное значение (для чисел)

`@Max(100) int age` Максимальное значение (для чисел)

`@Positive BigDecimal price` Число должно быть > 0

`@PositiveOrZero int quantity` Число ≥ 0

`@Negative BigDecimal balance` Число должно быть < 0

`@NegativeOrZero int delta` Число ≤ 0

`@Digits(integer=3, fraction=2)` Проверяет количество цифр (целая и дробная часть)

`@Past LocalDate birthDate` Дата должна быть в прошлом

`@PastOrPresent LocalDateTime createdAt` Дата в прошлом или сегодня

`@Future LocalDate expiryDate` Дата должна быть в будущем

`@FutureOrPresent LocalDate appointmentDate` Дата в будущем или сегодня

`@Email String email` Проверяет, что строка - валидный email

`@URL String website` Проверяет, что строка - валидный URL

`@CreditCardNumber String cardNumber` Проверяет номер кредитной карты (Luhn algorithm)

`@Length(min = 5, max = 20)` Аналог @Size (из Hibernate Validator)

`@NotEmpty List<String> tags` Коллекция/массив не должны быть пустыми

`@Size(min = 1, max = 10) List<Product> products` Проверяет размер коллекции/массива

# Где лучше задавать ограничения: в JPA-модели или в PostgreSQL?

| Способ              | Плюсы                            | Минусы                       | Когда использовать              |
|---------------------|----------------------------------|------------------------------|---------------------------------|
| Только в JPA        | Простота, переносимость между БД | Нет гарантии на уровне БД    | Простые проекты, прототипы      |
| Только в PostgreSQL | Максимальная надёжность          | Сложно поддерживать миграции | Критичные к данным системы      |
| Комбинированный     | Баланс надёжности и гибкости     | Дублирование кода            | Большинство production-проектов |

Hibernate

N+1 проблема в hibernate

Возникает, когда у родительской сущности есть дочерние сущность. Приложение делает 1 запрос, чтобы взять родительскую сущность, а потом, чтобы подтянуть все дочерние сущность - выполняется по 1 запросу для каждой дочерней сущности. То есть сначала делает 1 select, чтобы взять всех родительских сущностей, а потом для каждой родительской сущности он будет делать отдельный select (с фильтром where на родительскую сущность).

Решение 1. Использовать LAZY (FetchType.LAZY)

Это решит проблему - только если нам не надо использовать дочерние сущности и мы не будем их вызывать. Иначе - просто отложено позже выполнятся n+1 запросов для всех дочерних сущностей по каждому отдельному select.

Решение 2. Использовать Join Fetch

Надо сделать чтобы Hibernate под капотом сделал sql запрос с join внутри. То есть надо сделать кастомный sql запрос с Join внутри и вызывать его - тогда будет выполняться 1 запрос.

@Repository
@ALLArgsConstructor
public class CustomClientRepository {

  @PersistenceContext
  private final EntityManager entityManager;

  public List<Client> findAllClientWithPayments) {
    String jpql = "select c from Client c join fetch c.payments";
    return entityManager.createQuery(jpql, Client.class)
           .getResultList();
  }

Так же можно и переопределить стандартный метод репозитория

public interface ClientRepository extends JpaRepository<Client, Integer>

  @Query("select c from Client c join fetch c.payments")
  @Override 
  List<Client> findAll();
}

Или просто сделать свой метод

public interface ClientRepository extends JpaRepository<Client, Integer>

  @Query("select c from Client c join fetch c.payments")
  List<Client> findAllWithPaments();
}

Решение 3. Использовать Entity Graph

EntityGraph - это разовый план загрузки - какие её поля (ссылки) надо подгрузить прямо сейчас.

Он не меняет ваши аннотации LAZY/EAGER навсегда - действует только на тот запрос/find, куда его передали.

Работает с: em.find(...), JPQL/Criteria SELECT ... FROM ...

@NamedEntityGraph(...) - именной граф, объявлен рядом с сущностью и доступен по имени. em.createEntityGraph(Vehicle.class) - программный (динамический) граф, собирается в рантайме.

Объявление:

@NamedEntityGraph(
  name = "Vehicle.withCoordinatesAdmin",
  attributeNodes = {
    @NamedAttributeNode("coordinates"), 
    @NamedAttributeNode("admin")        
  }
)
@Entity
class Vehicle { }

либо программный способ(собираем в рантайме)

EntityGraph<Vehicle> g = em.createEntityGraph(Vehicle.class);
g.addAttributeNodes("coordinates", "admin");

Как применить граф:

К em.find

EntityGraph<Vehicle> g = (EntityGraph<Vehicle>) em.getEntityGraph("Vehicle.withCoordinatesAdmin");
Map<String, Object> hints = Map.of("jakarta.persistence.loadgraph", g);
Vehicle v = em.find(Vehicle.class, id, hints);

К JPQL/Criteria

TypedQuery<Vehicle> q = em.createQuery("select v from Vehicle v where v.id in :ids", Vehicle.class);
q.setParameter("ids", ids);
q.setHint("jakarta.persistence.loadgraph", g);
List<Vehicle> list = q.getResultList();

or

em.createQuery("select v from Vehicle v where v.id in :ids", Vehicle.class)
  .setParameter("ids", ids)
  .setHint("jakarta.persistence.loadgraph", g)
  .getResultList();
List<Vehicle> list = q.getResultList();

Два режима: loadgraph vs fetchgraph

loadgraph - Атрибуты из графа — сразу грузим; остальные — как в маппинге (LAZY/EAGER как аннотировано). fetchgraph - Только атрибуты из графа грузим сразу; все прочие — считаем LAZY даже если аннотированы EAGER (работает как «строгий список» полей).

Вложенность: subgraph

Граф по умолчанию действует только на один уровень: Vehicle.admin. Если нужно глубже, добавить подграф:

@NamedEntityGraph(
  name = "Vehicle.withAdminAndOrg",
  attributeNodes = {
    @NamedAttributeNode(value = "admin", subgraph = "admin.sg"),
    @NamedAttributeNode("coordinates")
  },
  subgraphs = {
    @NamedSubgraph(
      name = "admin.sg",
      attributeNodes = { @NamedAttributeNode("organization") } // ещё один внутри Admin
    )
  }
)

не пихать @OneToMany mappedBy коллекции в NamedEntityGraph

Решение 4. Использовать Batch Size

Hibernate тогда не будет делать join, а будет так же подгружать лениво и будет объединять пачками - батчами с указанным размером (компромисс между join и загрузкой лениво).