EntityManager ThreadLocal с JPA в JSE

Я разрабатываю простой проект "Книжный магазин", используя Struts 1.3 + JPA (с Hibernate как провайдер непрерывности). Я не могу переключиться на Spring или любую другую более сложную среду разработки (например, Jboss), и я не могу использовать какой-либо метод, зависящий от Hibernate (например, Session class).

Учитывая тот факт, что я нахожусь в среде JSE, мне нужно явно управлять всем жизненным циклом EntityManager.

Объект Book определяется следующим образом:

@Entity
public class Book {

@Id private String isbn;
private String title;
private Date publishDate;

    // Getters and Setters
}

Я определил три класса Action, которые ответственны, соответственно, за загрузку всех экземпляров книги, извлечение экземпляра одной книги с помощью своего ISBN и объединение отдельной книги в БД.

Чтобы увеличить разделение проблем между кодом бизнес-логики и кодом доступа к данным, я ввел простой объект BookDAO, который отвечает за выполнение операций CRUD. В идеале все вызовы, связанные с доступом к данным, должны быть делегированы на уровень сохранения. Например, ListBookAction определяется следующим образом:

public class ListBookAction extends Action {

    private BookDAO dao = new BookDAO();

    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {

        // Retrieve all the books
        List<Book> books = dao.findAll();

        // Save the result set
        request.setAttribute("books", books);

        // Forward to the view
        return mapping.findForward("booklist");
    }

}

Для объекта BookDAO требуется доступ к экземпляру EntityManager для выполнения любой операции. Учитывая, что EntityManger не является потокобезопасным, я ввел вспомогательный класс с именем BookUnitSession, который инкапсулирует EntityManager в переменную ThreadLocal:

public class BookUnitSession {

    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("BookStoreUnit");
    private static final ThreadLocal<EntityManager> tl = new ThreadLocal<EntityManager>();

    public static EntityManager getEntityManager() {
        EntityManager em = tl.get();

        if (em == null) {
            em = emf.createEntityManager();
            tl.set(em);
        }
        return em;
    }

}

Все, кажется, работает, но у меня все еще есть некоторые проблемы. А именно:

  • Это лучшее решение? что является наилучшей практикой в ​​этом случае?
  • Мне все еще нужно явно закрыть оба EntityManager и EntityManagerFactory. Как я могу это сделать?

Спасибо

Ответ 1

В течение последних нескольких дней я разработал возможное решение. То, что я пытался построить с классом BookUnitSession, было фактически классом EntityManagerHelper:

public class EntityManagerHelper {

    private static final EntityManagerFactory emf; 
    private static final ThreadLocal<EntityManager> threadLocal;

    static {
        emf = Persistence.createEntityManagerFactory("BookStoreUnit");      
        threadLocal = new ThreadLocal<EntityManager>();
    }

    public static EntityManager getEntityManager() {
        EntityManager em = threadLocal.get();

        if (em == null) {
            em = emf.createEntityManager();
            threadLocal.set(em);
        }
        return em;
    }

    public static void closeEntityManager() {
        EntityManager em = threadLocal.get();
        if (em != null) {
            em.close();
            threadLocal.set(null);
        }
    }

    public static void closeEntityManagerFactory() {
        emf.close();
    }

    public static void beginTransaction() {
        getEntityManager().getTransaction().begin();
    }

    public static void rollback() {
        getEntityManager().getTransaction().rollback();
    }

    public static void commit() {
        getEntityManager().getTransaction().commit();
    } 
}

Такой класс гарантирует, что каждый поток (т.е. каждый запрос) получит свой собственный экземпляр EntityManager. Следовательно, каждый объект DAO может получить правильный экземпляр EntityManager, вызвав EntityManagerHelper.getEntityManager()

В соответствии с шаблоном для каждого запроса каждый запрос должен открыть и закрыть свой собственный экземпляр EntityManager, который будет отвечать за инкапсуляцию требуемой единицы работы внутри транзакции. Это можно сделать с помощью фильтра перехвата, реализованного как ServletFilter:

public class EntityManagerInterceptor implements Filter {

    @Override
    public void destroy() {}

    @Override
    public void init(FilterConfig fc) throws ServletException {}

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
            FilterChain chain) throws IOException, ServletException {

            try {
                EntityManagerHelper.beginTransaction();
                chain.doFilter(req, res);
                EntityManagerHelper.commit();
            } catch (RuntimeException e) {

                if ( EntityManagerHelper.getEntityManager() != null && EntityManagerHelper.getEntityManager().isOpen()) 
                    EntityManagerHelper.rollback();
                throw e;

            } finally {
                EntityManagerHelper.closeEntityManager();
            }
    }
}

Этот подход позволяет также просматривать (например, страницу JSP) выборку полей сущностей, даже если они были инициализированы ленивым способом (Open Session in View). В среде JSE EntityManagerFactory должно быть явно закрыто, когда контейнер сервлета завершен. Это можно сделать, используя объект ServletContextListener:

public class EntityManagerFactoryListener implements ServletContextListener {

    @Override
    public void contextDestroyed(ServletContextEvent e) {
        EntityManagerHelper.closeEntityManagerFactory();
    }

    @Override
    public void contextInitialized(ServletContextEvent e) {}

}

Дескриптор развертывания web.xml:

<listener>
  <description>EntityManagerFactory Listener</description>
  <listener-class>package.EntityManagerFactoryListener</listener-class>
</listener>

<filter>
  <filter-name>interceptor</filter-name>
  <filter-class>package.EntityManagerInterceptor</filter-class>
</filter>

<filter-mapping>
  <filter-name>interceptor</filter-name>
  <url-pattern>*.do</url-pattern>
</filter-mapping>

Ответ 2

ScopedEntityManager Вспомогательный инструмент, который я создал в Github, использует подобную технику. Вместо фильтра запроса я выбрал ServletRequestListener для управления жизненным циклом. Также я не использую threadlocal, потому что у них есть привычка к утечкам памяти в контейнерах J2EE, если они не запрограммированы тщательно. У Tomcat есть некоторые трюки, чтобы предотвратить некоторые человеческие ошибки.