Как предотвратить повторное использование EntityManager с помощью guice-persist и @Transactional?

Согласно этому вопросу, когда используя guice-persist, EntityManager относится к области транзакции. Если я правильно понимаю, это означает, что для каждой транзакции будет создаваться новый EntityManager. При использовании guice-persist предлагается использовать JpaPersistModule, который предоставляет все привязки, и просто внедрить Provider<EntityManager> в некоторый класс, например:

public class ProjectDAO {

  private final Provider<EntityManager> entityManagerProvider;

  @Inject
  public ProjectDAO(Provider<EntityManager> entityManagerProvider) {

    this.entityManagerProvider = entityManagerProvider;
  }
} 

Примечание: в этом ответе говорится, что EntityManager не следует вводить напрямую, а использовать Provider<EntityManager> вместо этого, чтобы избежать этой проблемы, поэтому внедрение Provider<EntityManager>. Кроме того, просмотрев код для JpaPersistService, EntityManager экземпляров хранятся в ThreadLocal. В то же время аннотация @Transactional и ее аналог JpaLocalTxnInterceptor должны обеспечивать вызов .set() и .remove() в поле ThreadLocal<EntityManager> после каждой транзакции.

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

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

Проект (простой объект)

    @NamedQueries({
        @NamedQuery(name = "project.findAll", query = "from project"),
        @NamedQuery(name = "project.deleteByProjectName", query = "delete from project p where p.name = :project_name")
    }
    )
    @Entity(name = "project")
    public class Project {

        @Id
        @GeneratedValue
        private Long id;

        @Column(name="name")
        private String name;

        // ... getters/setters
    }

ПроектDAO

    public class ProjectDAO {

      private final Provider<EntityManager> entityManagerProvider;

      @Inject
      public ProjectDAO(Provider<EntityManager> entityManagerProvider) {
        this.entityManagerProvider = entityManagerProvider;
      }

      public void insert(Project project) {
        entityManagerProvider.get().persist(project);
      }

      public List<Project> findAll() {

        return entityManagerProvider.get()
            .createNamedQuery("project.findAll", Project.class)
            .getResultList();
      }

      public void delete(String projectName) {

        entityManagerProvider.get()
            .createNamedQuery("project.deleteByProjectName")
            .setParameter("project_name", projectName)
            .executeUpdate(); 
      }

      public Project findById(Long id) {

        return entityManagerProvider.get().find(Project.class, id);
      }
    }

ПроектСервис

    public class ProjectService {

      private final ProjectDAO projectDAO;

      @Inject
      public ProjectService(ProjectDAO projectDAO) {

        this.projectDAO = projectDAO;
      }

      @Transactional
      public void addProject(Project project) {
        projectDAO.insert(project);
      }

      @Transactional
      public List<Project> findAll() {
        return projectDAO.findAll();
      }

      @Transactional
      public void delete(String projectName) {
        projectDAO.delete(projectName);
      }

      @Transactional
      public Project findById(Long id) {
        return projectDAO.findById(id);
      }

      public EntityManager getEntityManager() {
        return projectDAO.getEntityManager();
      }
    }

Основной класс

    public class Start {

      public static void main(String[] args) throws InterruptedException {

        Injector injector = Guice.createInjector(new AbstractModule() {
          @Override 
          protected void configure() {
            install(new JpaPersistModule("hibernatetesting"));
            bind(ProjectService.class).in(Scopes.SINGLETON);
          }
        });

        ProjectService projectService = injector.getInstance(ProjectService.class);
        PersistService persistService = injector.getInstance(PersistService.class);

        persistService.start();

        // For the purpose of making transactions from different threads, we
        // create two single threaded executors
        ExecutorService executorService1 = Executors.newSingleThreadExecutor();

        ExecutorService executorService2 = Executors.newSingleThreadExecutor();

        // Execute a few queries from Thread 1
        CountDownLatch countDownLatch1 = new CountDownLatch(1);

        executorService1.execute(() -> {
        System.out.println("TEST: " + Thread.currentThread().getName());
        System.out.println("TEST: " +"EntityManager: " + projectService.getEntityManager().hashCode());
          projectService.addProject(new Project("project1"));
          projectService.addProject(new Project("project2"));
          countDownLatch1.countDown();
        });

        countDownLatch1.await();


        // Execute a few queries from Thread 2
        CountDownLatch countDownLatch2 = new CountDownLatch(1);

        executorService2.execute(() -> {
          System.out.println("TEST: " + Thread.currentThread().getName());
          System.out.println("TEST: " +"EntityManager: " + projectService.getEntityManager().hashCode());
          projectService.addProject(new Project("project3"));
          projectService.addProject(new Project("project4"));

          //----
          projectService.delete("project1");
          //----

          // project3 is not shown in this list
          projectService.findAll().forEach(System.out::println);
          countDownLatch2.countDown();
        });

        countDownLatch2.await();

        // Execute a few more queries from Thread 1
        CountDownLatch countDownLatch3 = new CountDownLatch(1);

        executorService1.execute(() -> {
          System.out.println("TEST: " + Thread.currentThread().getName());
          System.out.println("TEST: " +"EntityManager: " + projectService.getEntityManager().hashCode());
          projectService.addProject(new Project("project5"));
          projectService.addProject(new Project("project6"));

          // project3, which was deleted in Thread 2 is still visible in
          // this EntityManager
          // ----
          Project project = projectService.findById(3L);
          System.out.println("Project still exists " + project);
          // ----

          projectService.findAll().forEach(System.out::println);
          countDownLatch3.countDown();
        });

        countDownLatch3.await();

      }
    }

pom.xml

    ...
    <dependencies>

      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.11.Final</version>
      </dependency>

      <dependency>
        <groupId>com.google.inject</groupId>
        <artifactId>guice</artifactId>
        <version>4.2.2</version>
      </dependency>

      <dependency>
        <groupId>com.google.inject.extensions</groupId>
        <artifactId>guice-persist</artifactId>
        <version>4.2.2</version>
      </dependency>

      <dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
        <version>2.5.0</version>
      </dependency>

      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.11.Final</version>
      </dependency>

    </dependencies>
    ...

persistence.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="hibernatetesting" transaction-type="RESOURCE_LOCAL">
      <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

      <properties>
        <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
        <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:testDB"/>

        <property name="hibernate.show_sql" value="true" />
        <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
        <property name="hibernate.hbm2ddl.auto" value="create" />

      </properties>
    </persistence-unit>
    </persistence>

1) Это обычный способ использования EntityManager с guice-persist и обхода того факта, что разные потоки могут иметь разное состояние?

2) Если нет, то как убедиться, что EntityManager повторно устанавливается в ThreadLocal после каждой транзакции?


person Danilo Radenovic    schedule 20.08.2019    source источник


Ответы (1)


В приведенном выше коде есть две проблемы:

1) Следующая строка

System.out.println("TEST: " +"EntityManager: " + projectService.getEntityManager().hashCode());

был добавлен в целях отладки. Однако метод ProjectService.getEntityManager(), который вызывает ProjectDAO.getEntityManager(), который, в свою очередь, вызывает entityManagerProvider.get(), не имеет аннотации @Transactional. Это приводит к тому, что EntityManager устанавливается один раз для каждого потока и никогда не сбрасывается, даже если другие методы из ProjectService, имеющие аннотацию @Transactional, вызываются позже. Простое добавление этой аннотации решает проблему.

2) В одном потоке была удалена сущность с именем "project1"

   //----
   projectService.delete("project1");
   //----

однако в другом потоке проверяется присутствие другого объекта

   // project3, which was deleted in Thread 2 is still visible in this EntityManager
   Project project = projectService.findById(3L);
   System.out.println("Project still exists " + project);

который никогда не удалялся в первую очередь. Объекты добавляются по одному - проект1, проект2, проект3... и им присваиваются идентификаторы 1, 2, 3... соответственно. Так что код должен быть

   // project1, which was deleted in Thread 2 is still visible in this EntityManager
   Project project = projectService.findById(1L);
   System.out.println("Project still exists " + project);
person Danilo Radenovic    schedule 26.08.2019