javahibernatejpaguiceguice-persist

How to prevent reuse of EntityManager with guice-persist and @Transactional?


According to this question, when using guice-persist, EntityManager is transaction-scoped. If I understand correctly, this means that a new EntityManager will be created for every transaction. When using guice-persist, it is suggested to use JpaPersistModule, which provides all the bindings, and simply inject Provider<EntityManager> into some class, like this:

public class ProjectDAO {

  private final Provider<EntityManager> entityManagerProvider;

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

    this.entityManagerProvider = entityManagerProvider;
  }
} 

Note: in this answer it says that EntityManager should not be injected directly, but to use Provider<EntityManager> instead, to avoid this issue, therefore the injection of Provider<EntityManager>. Also, by looking at the code for JpaPersistService, EntityManager instances are stored in a ThreadLocal. At the same time, @Transactional annotation and its JpaLocalTxnInterceptor counterpart should ensure that .set() and .remove() are called on ThreadLocal<EntityManager> field after every transaction.

Now, I've tried this and every thread has its own EntityManager. However, it seems like it is not removed and set again, but reused for subsequent transactions, i.e. Hibernate's first level cache is not cleared.

Here's a complete example that inserts and deletes some entities from two different threads (sequentially, not in parallel), which leads to one thread having stale information:

Project (a simple entity)

    @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
    }

ProjectDAO

    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);
      }
    }

ProjectService

    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();
      }
    }

Main class

    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) Is this the usual way of using EntityManager with guice-persist and working around the fact that different threads might have different state?

2) If not, how to make sure that EntityManager is re-set on the ThreadLocal after each transaction?


Solution

  • There are two problems in the above code:

    1) The following line

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

    was added for debugging purposes. However, the method ProjectService.getEntityManager(), which calls ProjectDAO.getEntityManager(), which in turn calls entityManagerProvider.get(), is not annotated with @Transactional. This causes EntityManager to be set once per thread and never unset, even though other methods from ProjectService that have the @Transactional annotation are called later. Simply adding this annotation solves the problem.

    2) In one thread, entity with name "project1" was deleted

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

    however, in the other thread, presence is verified for another entity

       // 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);
    

    which was never deleted in the first place. Entities are added one by one - project1, project2, project3... and they are assigned IDs 1, 2, 3... respectively. So the code should be

       // 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);