In some of previous article we discovered how Hibernate and its JPA implementation work. We can use them as components for native-developed solutions. But we can also use them with Spring Framework.
A virtual conference at the intersection of Data and AI. This is not a conference for the hype. Its real users talking about real experiences.
- 40+ speakers with the likes of Hannes from Duck DB, Sol Rashidi, Joe Reis, Sadie St. Lawrence, Ryan Wolf from nvidia, Rebecca from lidl
- 12th September 2024
- Three simultaneous tracks
- Panels, Lighting Talks, Keynotes, Booth crawls, Roundtables and Entertainment.
- Topics include (ingestion, finops for data, data for inference (feature platforms), data for ML observability
- 100% virtual and 100% free
👉 Register here
This time we'll focus on using JPA with Spring Framework. We don't explain again the JPA concepts. If you don't remember them, please back to the article about Hibernate with JPA and come back here after reading it. Instead of reminding it, we'll start by configuring JPA with Spring's ORM package. After the configuration, we'll investigate what happens under the hood when EntityManagerFactory is created. After we'll focus on thread-safety of Spring's entity managers used through @PersistenceContext annotation.
Configure JPA in Spring
Configuring entity manager factory beans isn't different from configuring normal Spring beans. Simply, it can support different properties. Take a look on our sample:
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" /> </property> <property name="packagesToScan" value="com.mysite.data"/> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect <prop key="hibernate.show_sql">true </props> </property> </bean>
They are some explanations for different properties:
- dataSource a reference to datasource bean, ie. a bean which handles database connection with some supplementary features as connection pooling. In our case, the code of datasource bean looks like:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close" p:driverClass="com.mysql.jdbc.Driver" p:jdbcUrl="jdbc:mysql://localhost:3306/testing" p:user="root" p:password="" p:acquireIncrement="5" p:idleConnectionTestPeriod="60" p:maxPoolSize="100" p:maxStatements="50" p:minPoolSize="10" />
- jpaVendorAdapter allows to specify the implementation for JPA layer. The specified provider must implement org.springframework.orm.jpa.JpaVendorAdapter interface. In our sample we opt for HibernateJpaVendorAdapter which will be described further.
- packagesToScan this attribute specifies the package containing a list of entities to submit to entity manager.
- jpaProperties thanks to this properties list, we can configure internally JPA's provider or JPA layer.
LocalContainerEntityManagerFactoryBean lifecycle
This class, placed in org.springframework.orm.jpa package, extends an abstract class AbstractEntityManagerFactoryBean. As we can read in the JavaDoc for this last class, it's used to create an instance of EntityManagerFactory in Spring's application context. We retrieve there a lot of setters, invoked at bean creation and defined in the previous part:
- setJpaProperties: called for jpaProperties attribute.
- setJpaVendorAdapter: called for the bean's attribute named jpaVendorAdapter.
Because AbstractEntityManagerFactoryBean implements org.springframework.beans.factory.InitializingBean interface, it overrides afterPropertiesSet() method. It's invoked directly after the set of all bean properties. Inside this method, our abstract class ensures that all needed objects to handle JPA are defined. So, it gets persistence provider and entity manager factory interface directly from jpaVendorAdapter. Next, it calls createNativeEntityManagerFactory.
This method is defined as abstract and is overridden by all AbstractEntityManagerFactoryBean subclasses. The role of this method is to initialize EntityManagerFactory for set configuration. In our case, LocalContainerEntityManagerFactoryBean creates the instance of this class based on createEntityManagerFactory defined in org.hibernate.jpa.HibernatePersistenceProvider.
Freshly created EntityManagerFactory instance, called "native entity manager factory", is used further to generate "proxied entity manager factory". We can observe that when we try to get EntityManagerFactory bean through application context, as here:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations={"classpath:applicationContext-test.xml"}) @TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) public class JpaRepositoryTest { @Autowired private ApplicationContext context; @Test public void isEmfProxied() { assertTrue("EntityManagerFactory should be a proxied bean but it wasn't", context.getBean("emf").getClass().toString().contains("Proxy")); } }
If we try to print a class name of "emf" bean, we get a string like "class com.sun.proxy.$Proxy28". Thanks to it, Spring is able to return transaction-aware proxies for EntityManager.
Proxied entity manager for @PersistenceContext
Thanks to this transaction-aware proxies, EntityManager injected as @PersistenceContext can be used as a thread-safe object. It means that each requests will be handled by its own EntityManager. Why EntityManager is used through a proxy ? By definition (JSR-317 Final Release, section "7.3 Obtaining an Entity Manager Factory"), EntityManager is not thread safe. But according to the Spring's documentation, EntityManager injected through @PersistenceContext is "thread-safe proxy for the actual transactional EntityManager".
If you don't believe to the documentation, you can provoke an Exception by injecting persistence context and getting transaction manually (forbidden when using Spring's transactions):
@PersistenceContext private EntityManager em; // ... inside one method em.getTransaction();
The call of getTransaction will throw an IllegalStateException with given message:
java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:198) at com.sun.proxy.$Proxy38.getTransaction(Unknown Source) at com.waitingforcode.test.JpaRepositoryTest.sharedContext(JpaRepositoryTest.java:45) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
If we go to the SharedEntityManagerCreator.java file and look at line number 198, we'll see the override of invoke method. It contains some check to verify the methods invoked through the proxy and one of them, getTransaction, always throw an IllegalStateException, with exactly the same error message as our:
else if (method.getName().equals("getTransaction")) { throw new IllegalStateException( "Not allowed to create transaction on shared EntityManager - " + "use Spring transactions or EJB CMT instead"); }
Further in the code we can read that the doGetTransactionalEntityManager method of EntityManagerFactoryUtils (from the same package) is invoked to get current entity manager. In they're no entity manager already created, a new instance is initialized. Finally, initially called method is invoked by corresponding entity manager.
Thread-safety proof of Spring's @PersistenceContext
Notice that you can inject an no thread-safe persistence context by using type="PersistenceContextType.EXTENDED" attribute in @PersistenceContext annotation. We'll prove that through some sample code. First, let's make a service like that:
@Service("productService") public class ProductServiceImpl implements ProductService { @PersistenceContext private EntityManager persistenceContext; @Override @Transactional public void testPersistenceContext() { long time = System.nanoTime(); Session session = persistenceContext.unwrap(Session.class); System.out.println("["+time+"] Session's instance is : "+ session); System.out.println("["+time+"] Session's instance's hashCode equals to: "+ session.hashCode()); checkAnotherEm(time); } @Transactional private void checkAnotherEm(long time) { Session session = persistenceContext.unwrap(Session.class); System.out.println("["+time+"] Session's instance's hashCode from " + " checkAnotherEm equals to: "+session.hashCode()); } }
This code will be invoked inside one controller:
// ... @Autowired private ProductService productService; @RequestMapping(value = "persistence") public String testPersistenceContext() { productService.testPersistenceContext(); return "success"; } // ...
Now, let's make an test case and observe what is written into the logs:
// we still use the same test configuration with a // supplementary @WebAppConfiguration annotation // ... @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test public void sharedContext() { this.mockMvc.perform(get("/persistence")); this.mockMvc.perform(get("/persistence")); this.mockMvc.perform(get("/persistence")); } // ...
After executing this code, you can read from the logs following entries:
[21938940200259] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [21938940200259] Session's instance's hashCode equals to: 1776952746 [21938940200259] Session's instance's hashCode from checkAnotherEm equals to: 1776952746 [21938950870854] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [21938950870854] Session's instance's hashCode equals to: 229461995 [21938950870854] Session's instance's hashCode from checkAnotherEm equals to: 229461995 [21938954404224] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [21938954404224] Session's instance's hashCode equals to: 359134399 [21938954404224] Session's instance's hashCode from checkAnotherEm equals to: 359134399
As we expected, each request has its own transaction handled by exactly one EntityManager. In this case, EntityManager is wrapped to Hibernate's Session class. By analyzing theirs hashCodes we can check if they are one instance for all requests or not. But what happens if we change persistence context type to EXTENDED, like that:
@PersistenceContext(type = PersistenceContextType.EXTENDED) private EntityManager persistenceContext;
Now, if you relaunch the same code, the persistence context's hashCode will be the same for all requests. So it won't be thread safe because the same instance will be used:
[22207107017642] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [22207107017642] Session's instance's hashCode equals to: 1299675041 [22207107017642] Session's instance's hashCode from checkAnotherEm equals to: 1299675041 [22207131374531] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [22207131374531] Session's instance's hashCode equals to: 1299675041 [22207131374531] Session's instance's hashCode from checkAnotherEm equals to: 1299675041 [22207137182849] Session's instance is : SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]]) [22207137182849] Session's instance's hashCode equals to: 1299675041 [22207137182849] Session's instance's hashCode from checkAnotherEm equals to: 1299675041
In this article we learned about JPA basics in Spring environment. We saw that entity manager factory is a simple bean which is proxied. Thanks to it, it can creates transaction-aware proxies for persistence contexts (@PersistenceContext) used in the application. These contexts are thread safe when they aren't of EXTENDED type.