Рассмотрим в качестве примера следующую ситуацию.
У нас есть класс 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 { .
}
В большинстве ситуаций нам не важно, с чем именно мы работаем (реальный объект или его прокси).
При запросе любого поля любого объекта прокси-объект будет автоматически инициализирован и мы получим ожидаемые данные.
Но если нам нужно узнать тип объекта, то все идет наперекосяк.
Давайте разберемся, почему это происходит. Основная проблема заключается в том, что Hibernate не является психическим и не может заранее знать (без выполнения запросов к базе данных), какой тип объектов содержится в списке.
Соответственно, создается список, содержащий прокси-объекты, унаследованные от Phone.
Когда наша команда впервые столкнулась с этой проблемой, мы немного изучили этот вопрос и поняли, что придется делать «костыль».
Ошибка произошла в методе службы, где нам нужно было точно знать, с каким из дочерних классов мы имеем дело.
Прямо перед этой проверкой мы реализовали еще одну: если объект является прокси-объектом, то он инициализируется.
После чего эта неприятная история была благополучно забыта.
Со временем проект разросся, а бизнес-логика усложнилась.
И вот настал момент, когда подобных костылей стало уже слишком много (мы поняли, что на третьем-четвертом костыле все будет не так).
Причем эта проблема стала возникать не только при запросе ленивого списка других объектов у одного объекта, но и при прямом запросе списка объектов из базы данных.
Очень не хотелось отказываться от ленивой загрузки, потому что.
Наша база данных и так сильно загружена.
Мы решили больше не смешивать архитектурные слои приложения и создать что-то более универсальное.
Наша схема применения
В этой схеме запросы к базе данных обрабатываются уровнем 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("from " + entityClass.getName(), entityClass).
getResultList()); } .
}
Решение первой проблемы прошло не так гладко.
Вышеупомянутый метод не подходит, поскольку отложенная загрузка обрабатывается непосредственно Hibernate. И мы пошли на небольшую уступку.
Во всех объектах, которые содержат ленивые списки объектов разных типов с одним и тем же родителем (например, «Пользователь со списком телефонов»), мы переопределяем геттеры для этих списков.
Пока списки не запрашиваются, все нормально.
Объект содержит только список прокси, и ненужные запросы не выполняются.
Когда список запрашивается, он инициализируется.
Пример реализации геттера для списка телефонов пользователя public class User {
.
@OneToMany(fetch = FetchType.LAZY) private List<Phone> phones = new ArrayList<Phone>(); public List<Phone> getPhones() { return ConverterUtil.unproxyList(phones); } } 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 при использовании списков, содержащих объекты разных типов (с одним и тем же родителем), используемых в моей команде.
Надеюсь, этот пример поможет кому-то в похожей ситуации.
Если вы знаете более оптимальный/красивый способ решения этой проблемы, буду рад добавить его в статью.
Теги: #java #hibernate #OOP #inheritance #instanceof #слои абстракций #java
-
Йотафон
19 Oct, 24 -
Мысли
19 Oct, 24 -
Groovy Вместо Java
19 Oct, 24