Org.hibernate.LazyInitializationException в com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel

Несмотря на FetchType.EAGER и JOIN FETCH, я получаю LazyInitalizationException, добавляя некоторые объекты в коллекцию @ManyToMany через компонент JSF UISelectMany, например, в моем случае <p:selectManyMenu>.

@Entity IdentUser, с FetchType.EAGER:

@Column(name = "EMPLOYERS")
@ManyToMany(fetch = FetchType.EAGER, cascade= CascadeType.ALL)
@JoinTable(name = "USER_COMPANY", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "COMPANY_ID") })
private Set<Company> employers = new HashSet<Company>();

@Entity Company, с FetchType.EAGER:

@ManyToMany(mappedBy="employers", fetch=FetchType.EAGER)
private List<IdentUser> employee;

JPQL, с JOIN FETCH:

public List<IdentUser> getAllUsers() {
    return this.em.createQuery("from IdentUser u LEFT JOIN FETCH u.employers WHERE u.enabled = 1 AND u.accountNonLocked=0 ").getResultList();
}

Компонент JSF UISelectMany, вызывающий исключение при отправке:

<p:selectManyMenu value="#{bean.user.employers}" converter="#{entityConverter}">
    <f:selectItems value="#{bean.companies}" var="company" itemValue="#{company}" itemLabel="#{company.name}"/>
</p:selectManyMenu>

Соответствующая часть трассировки стека:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:566)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:186)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:545)
    at org.hibernate.collection.internal.PersistentSet.add(PersistentSet.java:206)
    at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel(MenuRenderer.java:382)
    at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValue(MenuRenderer.java:129)
    at com.sun.faces.renderkit.html_basic.MenuRenderer.getConvertedValue(MenuRenderer.java:315)
    at org.primefaces.component.selectmanymenu.SelectManyMenuRenderer.getConvertedValue(SelectManyMenuRenderer.java:37)
    ...

Как это вызвано и как я могу его решить?

Ответы

Ответ 1

При отправке компонентам JSF UISelectMany необходимо создать новый экземпляр коллекции с предварительно заполненными и преобразованными значениями. Он не будет очищать и повторно использовать существующую коллекцию в модели, поскольку она может быть отражена в других ссылках на один и тот же сборник или может потерпеть неудачу с помощью UnsupportedOperationException, потому что коллекция не поддается изменению, например, полученная с помощью Arrays#asList() или Collections#unmodifiableList().

MenuRenderer, средство рендеринга UISelectManyUISelectOne), компоненты, ответственные за все это, по умолчанию создадут новый экземпляр коллекции на основе коллекция getClass().newInstance(). Это, в свою очередь, потерпит неудачу с LazyInitializationException, если getClass() возвращает реализацию Hibernate PersistentCollection, которая внутренне используется Hibernate для заполнения свойство коллекции объекта. Метод add() должен инициализировать базовый прокси-сервер через текущий сеанс, но там нет, потому что задание не выполняется в рамках метода транзакционных сервисов.

Чтобы переопределить это поведение по умолчанию MenuRenderer, вам необходимо явно указать FQN нужного типа коллекции с помощью атрибута collectionType компонента UISelectMany. Для свойства List вы хотите указать java.util.ArrayList и для свойства Set, вы хотите указать java.util.LinkedHashSet (или java.util.HashSet, если упорядочение не важно):

<p:selectManyMenu ... collectionType="java.util.LinkedHashSet">

То же самое относится ко всем другим компонентам UISelectMany, которые напрямую привязаны к сущности JPA, управляемой Hibernate. Например:

<p:selectManyCheckbox ... collectionType="java.util.LinkedHashSet">
<h:selectManyCheckbox ... collectionType="java.util.LinkedHashSet">
<h:selectManyListbox ... collectionType="java.util.LinkedHashSet">
<h:selectManyMenu ... collectionType="java.util.LinkedHashSet">

См. также документация VDL среди других <h:selectManyMenu>. К сожалению, это не указано в документации VDL <p:selectManyMenu>, но поскольку они используют один и тот же рендерер для преобразования, он должен работать. Если IDE дергается неизвестным атрибутом collectionType и досадно подчеркивает его, даже если он работает, когда вы игнорируете его, используйте вместо него <f:attribute>.

<p:selectManyMenu ... >
    <f:attribute name="collectionType" value="java.util.LinkedHashSet" />
    ...
</p:selectManyMenu>

Ответ 2

Решение: замените editUserBehavior.currentUser.employers на коллекцию, которая не управляется Hibernate.

Почему? Когда Entity становится управляемым, Hibernate заменяет ваш HashSet своей собственной реализацией Set (be it PersistentSet). Анализируя реализацию JSF MenuRenderer, оказывается, что в какой-то момент он создает новый Set рефлексивно. См. Комментарий в MenuRenderer.convertSelectManyValuesForModel()

//пытаемся отразить конструктор без аргументов и вызывать, если доступно

При построении PersistentSet initialize() вызывается и - поскольку этот класс предназначен только для вызова из Hibernate - LazyInitializationException, вызывается.

Примечание. Это только мое подозрение. Я не знаю ваших версий JSF и Hibernate, но это более вероятно.