Использование сущностей JPA в JSF.Какова наилучшая стратегия предотвращения исключения LazyInitializationException? - PullRequest
10 голосов
/ 15 июня 2011

Хотелось бы услышать экспертов по лучшей практике редактирования сущностей JPA из пользовательского интерфейса JSF.

Итак, пара слов о проблеме.

Представьте, что у меня есть постоянный объект MyEntity, и я выбираю его для редактирования. В слое DAO я использую

return em.find(MyEntity.class, id);

Возвращает экземпляр MyEntity с прокси на «родительских» объектах - представьте, что одним из них является MyParent. MyParent извлекается как приветствие прокси на @Access(AccessType.PROPERTY):

@Entity
public class MyParent {

    @Id
    @Access(AccessType.PROPERTY)    
    private Long id;
    //...
}

и MyEntity имеет ссылку на него:

@ManyToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.PROXY)
private MyParent myParent;

Пока все хорошо. В пользовательском интерфейсе я просто использую извлеченный объект напрямую, без создания каких-либо объектов-значений, и использую родительский объект в списке выбора:

<h:selectOneMenu value="#{myEntity.myParent.id}" id="office">
    <f:selectItems value="#{parents}"/>
</h:selectOneMenu>

Все отображается нормально, LazyInitializationException не происходит. Но когда я сохраняю объект, я получаю

LazyInitializationException: could not initialize proxy - no Session

на MyParent прокси setId() метод.

Я легко могу решить проблему, если поменяю отношение MyParent на EAGER

@ManyToOne(fetch = FetchType.EAGER)
private MyParent myParent;

или получить объект, используя left join fetch p.myParent (на самом деле, это то, что я делаю сейчас). В этом случае операция сохранения работает нормально, и отношение прозрачно изменяется на новый объект MyParent. Никаких дополнительных действий (ручные копии, ручные настройки ссылок) не требуется. Очень просто и удобно.

НО . Если объект ссылается на 10 других объектов - em.find() приведет к 10 дополнительным соединениям , что не очень хорошая операция с БД, особенно когда Я вообще не использую состояние объектов ссылок . Все что мне нужно - это ссылки на объекты, а не их состояние.

Это глобальная проблема, я хотел бы знать, как специалисты JSF работают с сущностями JPA в своих приложениях, что является лучшей стратегией, позволяющей избежать как дополнительных объединений, так и LazyInitializationException.

Расширенный контекст персистентности мне не подходит.

Спасибо!

Ответы [ 6 ]

4 голосов
/ 14 августа 2015

Вы должны предоставить именно ту модель, которую ожидает представление.

Если сущность JPA соответствует точно необходимой модели, просто используйте ее прямо сейчас.

Если у сущности JPA слишком мало или слишком много свойств, используйте выражение конструктора DTO (подкласс) и / или с более конкретным запросом JPQL, при необходимости с явным FETCH JOIN. Или, возможно, со специфичными для Hibernate выборочными профилями или EclipseLink специфическими группами атрибутов . В противном случае это может вызвать ленивые исключения инициализации во всех местах или потреблять больше памяти, чем необходимо.

Шаблон "открытый сеанс в поле зрения" - плохой дизайн. Это подразумевает, что можно выполнять бизнес-логику при отображении ответа. Это не очень хорошо сочетается, в частности, с обработкой исключений, при которой цель состоит в том, чтобы показать пользовательскую страницу ошибки конечному пользователю. Если бизнес-исключение выдается на полпути при отображении ответа, в результате чего конечный пользователь уже получил заголовки ответа и часть HTML-кода, то сервер больше не может очистить ответ, чтобы отобразить красивую страницу с ошибкой. Кроме того, выполнение бизнес-логики в методах получения является неодобрительной практикой в ​​JSF согласно Почему JSF вызывает геттеры несколько раз .

Просто подготовьте именно ту модель, которая необходима представлению, с помощью обычных вызовов сервисных методов в управляемых компонентах действия / слушателя, прежде чем начнется фаза ответа рендеринга. Например, обычная ситуация - иметь под рукой существующую (неуправляемую) родительскую сущность с лениво загруженным дочерним свойством «один ко многим», и вы хотите отобразить его в текущем представлении с помощью действия ajax, тогда вам просто нужно позвольте методу прослушивания ajax извлечь и инициализировать его на уровне сервиса.

<f:ajax listener="#{bean.showLazyChildren(parent)}" render="children" />
public void showLazyChildren(Parent parent) {
    someParentService.fetchLazyChildren(parent);
}
public void fetchLazyChildren(Parent parent) {
    parent.setLazyChildren(em.merge(parent).getLazyChildren()); // Becomes managed.
    parent.getLazyChildren().size(); // Triggers lazy initialization.
}

В частности, в компонентах JSF UISelectMany есть еще одна, совершенно неожиданная, вероятная причина для LazyInitializationException: во время сохранения выбранных элементов JSF необходимо заново создать базовую коллекцию перед заполнением ее выбранными элементами, однако, если это происходит чтобы быть специфической реализацией отложенного загрузки коллекции для уровня сохраняемости, это исключение также будет выброшено. Решение состоит в том, чтобы явно установить для атрибута collectionType компонента UISelectMany требуемый «обычный» тип.

<h:selectManyCheckbox ... collectionType="java.util.ArrayList">

Подробно об этом и ответе в org.hibernate.LazyInitializationException на com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel .

Смотри также:

2 голосов
/ 17 августа 2015

Для Hibernate> = 4.1.6 читать https://stackoverflow.com/a/11913404/3252285

Использование фильтра OpenSessionInView (шаблон проектирования) очень полезно, но, на мой взгляд, это не решает проблему полностью, вот почему:

Если у нас есть сущность, сохраненная в Session или обработанная сессионным компонентом или извлеченная из кэша, и одна из его коллекций не была инициализирована во время того же запроса на загрузку, , тогда мы можем получить исключение в любое время мы называем это позже , даже если мы используем шаблон OSIV desing.

Давайте подробно рассмотрим проблему:

  • Для корректной работы любой прокси-сервер hibernate необходимо подключить к открытому сеансу.
  • Hibernate не предлагает никакого инструмента (Listener or Handler) для повторного подключения прокси-сервера в случае, если его сеанс закрыт или он отключен от собственного сеанса.

Почему спящий режим не предлагает этого? : поскольку определить, к какому сеансу не так просто подключить прокси-сервер, мы можем повторно подключиться, но во многих случаях это возможно.

Так как же подключить прокси, когда возникает исключение LazyInitializationException?.

В моем ERP я изменяю эти классы: JavassistLazyInitializer и AbstractPersistentCollection, тогда я больше не беспокоюсь об этом исключении (используется от 3 лет без ошибок):

class JavassistLazyInitializer{
     @Override
     public Object invoke(
                        final Object proxy,
                        final Method thisMethod,
                        final Method proceed,
                        final Object[] args) throws Throwable {
            if ( this.constructed ) {
                Object result;
                try {
                    result = this.invoke( thisMethod, args, proxy );
                }
                catch ( Throwable t ) {
                    throw new Exception( t.getCause() );
                }           
                if ( result == INVOKE_IMPLEMENTATION ) {
                    Object target = null;
                    try{
                        target = getImplementation();
                    }catch ( LazyInitializationException lze ) {
              /* Catching the LazyInitException and reatach the proxy to the right Session */
                    EntityManager em = ContextConfig.getCurrent().getDAO(
                                        BaseBean.getWcx(), 
                                        HibernateProxyHelper.getClassWithoutInitializingProxy(proxy)).
                                        getEm();
                                ((Session)em.getDelegate()).refresh(proxy);// attaching the proxy                   
                    }   
                    try{                
                        if (target==null)
                            target = getImplementation();
                            .....
                    }
        ....
     }

и

class AbstractPersistentCollection{
private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) {
        SessionImplementor originalSession = null;
        boolean isTempSession = false;
        boolean isJTA = false;      
        if ( session == null ) {
            if ( allowLoadOutsideTransaction ) {
                session = openTemporarySessionForLoading();
                isTempSession = true;
            }
            else {
    /* Let try to reatach the proxy to the right Session */
                try{
                session = ((SessionImplementor)ContextConfig.getCurrent().getDAO(
                        BaseBean.getWcx(), HibernateProxyHelper.getClassWithoutInitializingProxy(
                        owner)).getEm().getDelegate());             
                SessionFactoryImplementor impl = (SessionFactoryImplementor) ((SessionImpl)session).getSessionFactory();            
                ((SessionImpl)session).getPersistenceContext().addUninitializedDetachedCollection(
                        impl.getCollectionPersister(role), this);
                }catch(Exception e){
                        e.printStackTrace();        
                }
                if (session==null)
                    throwLazyInitializationException( "could not initialize proxy - no Session" );
            }
        }
        if (session==null)
            throwLazyInitializationException( "could not initialize proxy - no Session" );
        ....
    }
...
}

Примечание:

  • Я не исправил все возможности, такие как JTA или другие случаи.
  • Это решение работает даже лучше, когда вы активируете кеш
2 голосов
/ 16 августа 2015

Lazy Loading - важная функция, которая может значительно повысить производительность.Однако юзабилити этого гораздо хуже, чем должно быть.

Особенно, когда вы начинаете работать с AJAX-запросами, сталкиваясь с неинициализированными коллекциями, Annotation ist just полезна, чтобы сообщить Hibernate , что не загружайте это прямо сейчас .Hibernate не заботится ни о чем другом, но бросит в вас LazyInitializationException - как вы уже испытали.

Мое решение этого - которое может быть не идеальным или кошмарным для всех - работает в любом сценарии,применяя следующие правила (я должен признать, что это было написано в самом начале, но работает с тех пор):

Каждая сущность, которая использует fetch = FetchType.LAZY , имеет для расширения LazyEntity и вызова initializeCollection() в получателе рассматриваемого collection, прежде чем он будет возвращен.(Пользовательский валидатор позаботится об этих ограничениях, сообщая о пропущенных расширениях и / или вызовах initializeCollection)

Example-Class (Пользователь, у которого группы загружаются лениво):

public class User extends LazyEntity{
     @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
     @BatchSize(size = 5)
     List<Group> groups; 

     public List<Group> getGroups(){
       initializeCollection(this.groups);
       return this.groups;
     }
}

Где реализация initializeCollection(Collection collection) выглядит следующим образом.Встроенные комментарии должны дать вам представление о том, что требуется для какого сценария.Метод синхронизирован, чтобы избежать 2 активных сеансов , передающих владение объектом, в то время как другой сеанс в настоящее время выбирает данные.(Отображается только в том случае, когда параллельные Ajax-запросы выполняются в одном и том же экземпляре.)

public abstract class LazyEntity {

    @SuppressWarnings("rawtypes")
    protected synchronized void initializeCollection(Collection collection) {
        if (collection instanceof AbstractPersistentCollection) {
             //Already loaded?
             if (!Hibernate.isInitialized(collection)) {
                AbstractPersistentCollection ps = (AbstractPersistentCollection) collection;

                //Is current Session closed? Then this is an ajax call, need new session!
                //Else, Hibernate will know what to do.
                if (ps.getSession() == null) {
                    //get an OPEN em. This needs to be handled according to your application.
                    EntityManager em = ContextHelper.getBean(ServiceProvider.class).getEntityManager();

                    //get any Session to obtain SessionFactory
                    Session anySession = em.unwrap(Session.class);
                    SessionFactory sf = anySession.getSessionFactory();

                    //get a new session    
                    Session newSession = sf.openSession();

                    //move "this" to the new session.
                    newSession.update(this);

                    //let hibernate do its work on the current session.
                    Hibernate.initialize(collection);

                    //done, we can abandon the "new Session".
                    newSession.close();
                }
            }
        }
    }
}

Но имейте в виду, что для этого подхода требуется проверка, если объект связан с сеансом CURRENT при каждом сохраненииэто - иначе вы должны переместить все дерево объектов в текущую сессию снова перед вызовом merge().

2 голосов
/ 15 июня 2011

Очень распространенным подходом является создание открытого менеджера сущностей в фильтре представления .Spring предоставляет один (отметьте здесь ).

Я не вижу, что вы используете Spring, но на самом деле это не проблема, вы можете адаптировать код в этом классе для своих нужд,Вы также можете проверить фильтр Открыть сеанс в представлении , который делает то же самое, но сохраняет сеанс гибернации открытым, а не Entity Manager.

Этот подход может не подойти для вашего приложенияЕсть несколько дискуссий в SO об этом паттерне или антипаттерне. Link1 .Я думаю, что для большинства приложений (небольших, менее 20 одновременно работающих пользователей) это решение работает просто отлично.

Edit

Здесь класс Spring лучше связывается с FSF здесь

1 голос
/ 17 августа 2015

Шаблон проектирования Open Session in View может быть легко реализован в среде Java EE (без зависимости от hibernate, spring или чего-либо еще вне Java EE). Он в основном такой же, как в OpenSessionInView , но вместо сеанса Hibernate следует использовать транзакцию JTA

@WebFilter(urlPatterns = {"*"})
public class JTAFilter implements Filter{

    @Resource
    private UserTransaction ut;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try{
           ut.begin();
           chain.doFilter(request, response);
        }catch(NotSupportedException | SystemException e){
            throw new ServletException("", e);
        } finally {
            try {
               if(ut.getStatus()!= Status.STATUS_MARKED_ROLLBACK){
                   ut.commit();
               }
            } catch (Exception e) {
                throw new ServletException("", e);
            }
       }
  }

  @Override
  public void destroy() {

  }
}
1 голос
/ 12 декабря 2013

В EJB3 отсутствует стандартная поддержка открытого сеанса, см. ответ .

Тип выборки сопоставлений - это просто опция по умолчанию, которую я могу переопределить во время запроса.Это пример:

select g from Group g fetch join g.students

Таким образом, альтернатива в простом EJB3 состоит в том, чтобы убедиться, что все данные, необходимые для рендеринга представления, загружены перед началом рендеринга, явно запрашивая необходимые данные.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...