Я Java розробник і в вільний час займаюсь розробкою власного застосунку про який думаю, ще напишу окремий довгочит. В цьому ж довгочиті я хотів би поділитись проблемою пов’язаною з наслідуванням в сутностях із якою я стикнувся під час розробки.


Як виглядає наслідування в сутностях і що це таке ?

Насамперед розповім про те, як взагалі виглядає наслідування в сутностях в Hibernate. Для прикладу побудуємо класичну ієрархію з фігурами.

Схемка з сутностями фігур

Ну і звичайно приклад коду створення цієї ієрархії:

@Entity(name = "shapes")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "shape_type", 
        discriminatorType = DiscriminatorType.STRING)
public class Shape {
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;
}
@Entity
@DiscriminatorValue("rectangle")
public class Rectangle extends Shape {
    private Double length;
    private Double width;
}
@Entity
@DiscriminatorValue("circle")
public class Circle extends Shape {
    private Double radius;
}

В базі даних за цим приладом створиться табличка:

Створена табличка “shapes” в бд

Існує, ще кілька варіантів організації цих сутностей в базі даних окрім однієї таблиці (об’єднані таблиці, окрема таблиця на сутність). Детальніше можна почитати тут - ТИЦЬ.

Почнемо експериментувати

На такому простому прикладі проблем немає і якщо ми спробуємо дістати всі фігури ми будемо мати лише один запит до бази даних.

Hibernate: select s1_0.id,s1_0.shape_type,s1_0.radius,s1_0.length,s1_0.width from shapes s1_0

Якщо навіть будемо використовувати інші схеми організації таблиць все одно буде один запит до бази даних але з об’єднанням.

Hibernate: select s1_0.id,s1_0.clazz_,s1_0.radius,s1_0.length,s1_0.width from ( 
      select id, null::float as radius, null::float as length, null::float as width, 0 as clazz_ from shapes 
union all 
      select id, radius, null::float as length, null::float as width, 1 as clazz_ from circle 
union all 
      select id, null::float as radius, length, width, 2 as clazz_ from rectangle ) s1_0

Проблеми починаються пізніше, якщо ми хочемо додати зв’язки з іншими таблицями в похідні сутності. Для прикладу створимо сутність з кольором і додамо звязок із прямокутником (для зручнішого перегляду sql запитів буду використовувати збереження сутностей в одній таблиці). А тепер глянемо, які запити виконаються, якщо спробувати дістати всі фігури:

Hibernate: select s1_0.id,s1_0.shape_type,s1_0.radius,s1_0.color_id,s1_0.length,s1_0.width from shapes s1_0
Hibernate: select c1_0.id,c1_0.name from colors c1_0 where c1_0.id=?
Hibernate: select c1_0.id,c1_0.name from colors c1_0 where c1_0.id=?
Hibernate: select c1_0.id,c1_0.name from colors c1_0 where c1_0.id=?
Hibernate: select c1_0.id,c1_0.name from colors c1_0 where c1_0.id=?
Hibernate: select c1_0.id,c1_0.name from colors c1_0 where c1_0.id=?

Ми бачимо класичний приклад N + 1 проблеми. Ми маємо один загальний запит і багато окремих запитів на те, щоб дістати з бази даних кольори для кожного з прямокутників.

Вирішення N + 1 проблеми

Ті хто використовують Hibernate мабуть знають, що є кілька основних способів щоб вирішити цю проблему.

  • @Fetch(FetchMode.JOIN) анотація, детальніше тут - ТИЦЬ.

  • Entity Graphs, детальніше тут - ТИЦЬ.

Обидва з цих методів не працюють якщо в вас є ієрархія в сутностях. Єдиний спосіб, що в мене працював це прописувати join fetch в анотаціях @Query в репозиторіях, наприклад:

public interface ShapeRepo extends JpaRepository<Shape, Long> {

    @Query("select s from shapes s left join fetch s.color")
    List<Shape> findAll();
}

Такий спосіб працює і в ми маємо лише один запит до бази даних з джойном:

select s1_0.id,s1_0.shape_type,s1_0.radius,c1_0.id,c1_0.name,s1_0.length,s1_0.width from shapes s1_0 left join colors c1_0 on c1_0.id=s1_0.color_id

Нова проблема

Але все не так просто, як здається здавалось би додаємо fetch до запитів і все працює добре, але ні… Давайте створимо сутність наприклад Картинка і зробимо звязок багато до багатьох з Колом. А зв’язок між Кольором і Прямокутником також змінимо на багато до багатьох.

Але якщо ми спробуємо за попередньою схемою зробити запит:

@Query("select s from shapes s left join fetch s.colors left join fetch s.images")
List<Shape> findAll();

То отримаємо помилку:

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.taraskovaliv.demo.entities.Circle.images, com.taraskovaliv.demo.entities.Rectangle.colors]

Рішення цієї проблеми для унаслідуваних сутностей я так і не знайшов.

Висновок

Підсумовуючи все вищескразане скажу що краще уникати наслідування в сутностях, якщо потрібно якось розділити бізнес логіку switch працює добре. Наслідування в сутностях викликає багато проблем з оптимізацією кількостів запитів до бази даних, що може привести до проблем якщо ваші дочірні сутності мають зв’язки з іншими базами даних.

P.S.

Підписуйтесь в подальшому планую писати, ще про всякі цікаві речі що зустрінуться мені під час розробки власного застосунку і про сам застосунок).

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Taras Kovaliv
Taras Kovaliv@taras_kovaliv

Java розробник з ціллю

247Прочитань
50Автори
22Читачі
Підтримати
На Друкарні з 15 квітня

Більше від автора

Вам також сподобається

Коментарі (4)

А навищо вам наслідування для Entities?

Вам також сподобається