Рассмотрим, в качестве примера, следующую ситуацию. У нас имеется класс 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 методы для инициализации одного прокси-объекта и инициализации списка прокси-объектов. При каждом запросе списка объектов из БД этот список проходит инициализацию (Мы сознательно пошли на такой шаг т.к. если мы запрашиваем какой-то список объектов в нашем приложении — то почти всегда он нужен полностью проинициализированным).
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. И мы пошли на небольшую уступку. Во всех объектах, содержащих ленивые списки разных типов объектов с одним родителем (например, User со списком Phone) мы переопределили геттеры для этих списков. Пока списки не запрашиваются — все в порядке. Объект содержит только прокси-список и не выполняются лишние запросы. При запросе списка происходит его инициализация.
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 при использовании списков, содержащих объекты разных типов (с одним родителем), используемый в моей команде. Надеюсь этот пример поможет кому-нибудь в аналогичной ситуации. Если вы знаете более оптимальный/красивый способ победить эту проблему, буду рад добавить его в статью.
Автор: Westimo