Я 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;
}
В базі даних за цим приладом створиться табличка:
Існує, ще кілька варіантів організації цих сутностей в базі даних окрім однієї таблиці (об’єднані таблиці, окрема таблиця на сутність). Детальніше можна почитати тут - ТИЦЬ.
Почнемо експериментувати
На такому простому прикладі проблем немає і якщо ми спробуємо дістати всі фігури ми будемо мати лише один запит до бази даних.
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 мабуть знають, що є кілька основних способів щоб вирішити цю проблему.
Обидва з цих методів не працюють якщо в вас є ієрархія в сутностях. Єдиний спосіб, що в мене працював це прописувати 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.
Підписуйтесь в подальшому планую писати, ще про всякі цікаві речі що зустрінуться мені під час розробки власного застосунку і про сам застосунок).