Hibernate: ледаче завантаження, спадкування і instanceof

Розглянемо, як приклад наступну ситуацію. У нас є клас User з полями, що описують користувача. Є клас Phone, який є батьківським для класів CellPhone і SatellitePhone. У класі User є поле містить список телефонів користувача. З метою зменшення навантаження на БД ми зробили цей список «ледачим». Він буде завантажуватися тільки на вимогу.

Виглядає це все приблизно так
public class User {
...

@OneToMany(fetch = FetchType.LAZY)
private List<Phone> phones = new ArrayList<Phone>();

public List<Phone> getPhones() {
return phones;
}
}

public class Phone {
...
}

public class CellPhone extends Phone {
...
}

public class SatellitePhone extends Phone {
...
}


У такій конфігурації при запиті списку телефонів конкретного користувача ми можемо отримати список проинициализированных об'єктів-телефонів (наприклад, якщо вони вже є в кеші), так і список proxy-об'єктів.
У більшості ситуацій нам не важливо з чим саме ми працюємо (реальним об'єктом або його proxy). При запиті будь-якого поля будь-якого об'єкта — proxy-об'єкт автоматично проинициализируется, і ми отримаємо очікувані дані. Але якщо нам потрібно визначити тип об'єкта, то все йде шкереберть.

Давайте розберемося, чому так відбувається. Основна проблема полягає в тому, що Hibernate — не екстрасенс і не може знати заздалегідь (не виконавши запити до БД) якого типу об'єкти містяться в списку. У відповідності з цим створює список, що містить proxy-об'єкти, успадковані від Phone.


Коли наша команда в перший раз зіткнулася з цією проблемою ми трохи вивчили дане питання і зрозуміли, що доведеться робити «милиця». Помилка виникала в сервісному методі де треба було точно знати з яким з дочірніх класів ми маємо справу. Ми прямо перед цією перевіркою запровадили іншу: якщо об'єкт є proxy-об'єктом, то він ініціалізується. Після чого благополучно забули цю неприємну історію.

З часом проект ріс, бізнес-логіка ускладнювалася. І ось настав момент, коли подібних милиць стало вже занадто багато (ми зрозуміли, що так справа не піде на третьому або четвертому костыле). Причому дана проблема стала виникати не тільки при запиті у одного об'єкта ледачого списку інших об'єктів, а й при прямому запиті з бази даних списку об'єктів. Відмовлятися від лінивої завантаження дуже не хотілося т. к. база у нас і так сильно навантажена. Ми вирішили більше не перемішувати архітектурні шари програми і створити щось більш універсальне.

Схема нашого додатка

В даній схемі запитів до БД займається DAO шар. Він складається з 1 абстрактного класу JpaDao в якому визначено всі базові методи по роботі з базою даних. І множини класів — його спадкоємців, кожен з яких в кінцевому підсумку використовує методи базового класу. Отже, як ми побороли проблему з прямим запитом списку об'єктів різних типів з загальним батьком? Ми створили в класі JpaDao методи для ініціалізації одного проксі-об'єкта і ініціалізації списку проксі-об'єктів. При кожному запиті списку об'єктів БД цей список проходить ініціалізацію (Ми свідомо пішли на такий крок, оскільки якщо ми запитуємо якийсь список об'єктів в нашому додатку — то майже завжди він потрібен повністю проинициализированным).

Приклад реалізації JpaDao
public abstract class JpaDao<ENTITY extends BaseEntity> {
...

private ENTITY unproxy(ENTITY entity) {
if (entity != null) {
if (entity instanceof HibernateProxy) {
Hibernate.initialize(entity);
entity = (ENTITY) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
}
}
return entity;
}

private List<ENTITY> unproxy(List<ENTITY> entities) {

boolean hasProxy = false;
for (ENTITY entity : entities) {
if (entity instanceof HibernateProxy) {
hasProxy = true;
break;
}
}

if (hasProxy) {
List<ENTITY> unproxiedEntities = new LinkedList<ENTITY>();
for (ENTITY entity : entities) {
unproxiedEntities.add(unproxy(entity));
}

return unproxiedEntities;
}

return entities;
}

...

public List<ENTITY> findAll() {
return unproxy(getEntityManager().createQuery("з " + entityClass.getName(), entityClass).getResultList());
}

...
}


З вирішенням першої проблеми все вийшло не так гладко. Вищеописаний спосіб не підходить так як ледачою завантаженням займається безпосередньо Hibernate. І ми пішли на невелику поступку. В усіх об'єктах, що містять ледачі списки різних типів об'єктів з одним батьком (наприклад, User зі списком Phone) ми переопределили геттери для цих списків. Поки списки не запитуються — все в порядку. Об'єкт містить лише проксі-список і не виконуються зайві запити. При запиті списку відбувається його ініціалізація.

Приклад реалізації геттера списку телефонів у користувача
public class User {
...

@OneToMany(fetch = FetchType.LAZY)
private List<Phone> phones = new ArrayList<Phone>();

public List<Phone> getPhones() {
return ConverterUtil.unproxyList(телефонів);
}
}

public class ConverterUtil {
...

public static <T> T unproxy(T entity) {
if (entity == null) {
return null;
}

Hibernate.initialize(entity);
if (entity instanceof HibernateProxy) {
entity = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
}
return entity;
}

public static <T> List<T> unproxyList(List<T> list) {
boolean hasProxy = false;
for (T entity : list) {
if (entity instanceof HibernateProxy) {
hasProxy = true;
break;
}
}

if (hasProxy) {
LinkedList<T> result = new LinkedList<T>();

for (T entity : list) {
if (entity instanceof HibernateProxy) {
result.add(ConverterUtil.unproxy(entity));
} else {
result.add(entity);
}
}

list.clear();
list.addAll(result);
}

return list;
}
}


У даній статті я продемонстрував спосіб використання ледачою завантаження Hibernate при використанні списків, що містять об'єкти різних типів (з одним батьком), використовуваний в моїй команді. Сподіваюся цей приклад допоможе кому-небудь в аналогічній ситуації. Якщо ви знаєте більш оптимальний/гарний спосіб перемогти цю проблему, буду радий додати його в статтю.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.