Lets assume that we have correctly configured jpa backed by hibernate(4.3.11) in spring(4.2.7). Hibernate first level cache is enabled. We use declarative transactions. We have OuterBean
@Service public class OuterBean { @Resource private UserDao userDao; @Resource private InnerBean innerBean; @Transactional(propagation = Propagation.NEVER) public void withoutTransaction(){ User user = userDao.load(1l); System.out.println(user.getName());//return userName innerBean.withTransaction(); user = userDao.load(1l); System.out.println(user.getName());//return userName instead of newUserName } }
And InnerBean that is called from OuterBean:
@Service public class InnerBean { @Resource private UserDao userDao; @Transactional public void withTransaction(){ User user = userDao.load(1l); user.setName("newUserName"); }
}
Is it correct behaviour that method user.getName()
in OuterBean returns the same value twice(second time is after update name in database)?
In other words is it correct behaviour that @Transactional(propagation = Propagation.NEVER)
creates hibernate session for method withoutTransaction()
that causes that second call user.getName()
reads from hibernate first level cache instead of database?
EDIT
To explain problem more I attache trace from creation of hibernate sessions
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@c17285e TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689173439 TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL userName TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@715c48ca TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689173439 TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Automatically flushing session TRACE org.hibernate.internal.SessionImpl - before transaction completion TRACE org.hibernate.internal.SessionImpl - after transaction completion TRACE org.hibernate.internal.SessionImpl - Closing session TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL userName TRACE org.hibernate.internal.SessionImpl - Closing session
Now let's compare trace when I remove @Transactional(propagation = Propagation.NEVER)
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@4ebd2c5f TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203905 TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Closing session userName TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@5af84083 TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203905 TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Automatically flushing session TRACE org.hibernate.internal.SessionImpl - before transaction completion TRACE org.hibernate.internal.SessionImpl - after transaction completion TRACE org.hibernate.internal.SessionImpl - Closing session TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@35f4f41f TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203906 TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL TRACE org.hibernate.internal.SessionImpl - Closing session newUserName
Please notice when I omit @Transactional(propagation = Propagation.NEVER)
separate session is crete for every invocation of method from userDao.
So my question can be formulated also as Should’t be @Transactional(propagation = Propagation.NEVER)
implemented in spring as guardian that prevent us for accidentally use transaction, without any side effect(session creation)?
4 Answers
Answers 1
The behavior is correct - Hibernate will always create a session (how else would you expect it to perform any operation?), and by loading the entity you have associated it with that session. Since withoutTransaction
is not participating in a transaction, the changes made within withTransaction
will happen within a new transaction and shouldn't be visible unless you call refresh
, which will force a re-load from the database.
I'm quoting Hibernate's official documentation:
The main function of the Session is to offer create, read and delete operations for instances of mapped entity classes. Instances may exist in one of three states:
- transient: never persistent, not associated with any Session
- persistent: associated with a unique Session detached: previously
- persistent, not associated with any Session
Transient instances may be made persistent by calling
save()
,persist()
orsaveOrUpdate()
. Persistent instances may be made transient by callingdelete()
. Any instance returned by aget()
orload()
method is persistent.
Taken from Java Persistence With Hibernate:
The persistence context acts as a first-level cache; it remembers all entity instances you’ve handled in a particular unit of work. For example, if you ask Hibernate to load an entity instance using a primary key value (a lookup by identifier), Hibernate can first check the current unit of work in the persistence context. If Hibernate finds the entity instance in the persistence context, no database hit occurs—this is a repeatable read for an application. Consecutive
em.find(Item.class, ITEM_ID)
calls with the same persistence context will yield the same result.
Concerning transactions, here's an excerpt taken from official Hibernate's documentation:
Defines the contract for abstracting applications from the configured underlying means of transaction management. Allows the application to define units of work, while maintaining abstraction from the underlying transaction implementation (eg. JTA, JDBC).
So, to sum it up, withTransaction
and withoutTransaction
will not share UnitOfWork and therefore will not share the first-level cache, which is why the second load returns the original value.
As to the reasons why these two methods do not share the unit of work, you can refer to Shailendra's answer.
EDIT:
You seem to misunderstand something. A session must always be created - that's how Hibernate works, period. Your expectation of no sessions being created is equal to expecting to execute a JDBC query without having a JDBC connection :)
The difference between your two examples is that with @Transactional(propagation = Propagation.NEVER)
your method is intercepted and proxied by Spring and only a single session is created for the queries in withoutTransaction
. When you remove the annotation you exclude your method from Spring's transactional interceptor so a new session will be created for each DB-related operation. I repeat again, and I cannot stress this enough - you must have an open session to perform any queries.
As far as guarding goes - try swapping the annotations on the two methods by making withTransaction
use Propagation.NEVER and withoutTransaction
use the default @Transactional
annotation and see what happens (spoiler: you'll get an IllegalTransactionStateException
).
EDIT2:
As for why the session is shared between two loads in the outer bean - that's just what JpaTransactionManager
is supposed to do, and by annotating your method with @Transactional
you've notified Spring that it should use the configured transaction manager to wrap your method. Here's what the official documentation says about JpaTransactionManager
's expected behavior:
PlatformTransactionManager implementation for a single JPA EntityManagerFactory. Binds a JPA EntityManager from the specified factory to the thread, potentially allowing for one thread-bound EntityManager per factory. SharedEntityManagerCreator and @PersistenceContext are aware of thread-bound entity managers and participate in such transactions automatically. Using either is required for JPA access code supporting this transaction management mechanism.
Also, to know how Spring is handling declarative transaction management (i.e. @Transactional
annotations on methods), refer to the official documentation. For ease of navigation, I'll include a quote:
The most important concepts to grasp with regard to the Spring Framework’s declarative transaction support are that this support is enabled via AOP proxies, and that the transactional advice is driven by metadata (currently XML- or annotation-based). The combination of AOP with transactional metadata yields an AOP proxy that uses a
TransactionInterceptor
in conjunction with an appropriatePlatformTransactionManager
implementation to drive transactions around method invocations.
Answers 2
@Transactional(propagation = Propagation.NEVER) would still create a session. If you are using Spring/Hibernate/JPA combination for non distributed transactions then you are most certainly using JpaTransactionManager as the Spring transaction manager. The answer to your question lies in this class. A good idea would be do use a debugger in your IDE to follow what's happening. The doBegin method of this class ( which is called by Spring transaction infrastructure is :-
protected void doBegin(Object transaction, TransactionDefinition definition) { JpaTransactionObject txObject = (JpaTransactionObject) transaction; if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) { throw new IllegalTransactionStateException( "Pre-bound JDBC Connection found! JpaTransactionManager does not support " + "running within DataSourceTransactionManager if told to manage the DataSource itself. " + "It is recommended to use a single JpaTransactionManager for all transactions " + "on a single DataSource, no matter whether JPA or JDBC access."); } try { if (txObject.getEntityManagerHolder() == null || txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) { EntityManager newEm = createEntityManagerForTransaction(); if (logger.isDebugEnabled()) { logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction"); } txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true); } EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); // Delegate to JpaDialect for actual transaction begin. final int timeoutToUse = determineTimeout(definition); Object transactionData = getJpaDialect().beginTransaction(em, new DelegatingTransactionDefinition(definition) { @Override public int getTimeout() { return timeoutToUse; } }); txObject.setTransactionData(transactionData); // Register transaction timeout. if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse); } // Register the JPA EntityManager's JDBC Connection for the DataSource, if set. if (getDataSource() != null) { ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly()); if (conHandle != null) { ConnectionHolder conHolder = new ConnectionHolder(conHandle); if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) { conHolder.setTimeoutInSeconds(timeoutToUse); } if (logger.isDebugEnabled()) { logger.debug("Exposing JPA transaction as JDBC transaction [" + conHolder.getConnectionHandle() + "]"); } TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); txObject.setConnectionHolder(conHolder); } else { if (logger.isDebugEnabled()) { logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because " + "JpaDialect [" + getJpaDialect() + "] does not support JDBC Connection retrieval"); } } } // Bind the entity manager holder to the thread. if (txObject.isNewEntityManagerHolder()) { TransactionSynchronizationManager.bindResource( getEntityManagerFactory(), txObject.getEntityManagerHolder()); } txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true); } catch (TransactionException ex) { closeEntityManagerAfterFailedBegin(txObject); throw ex; } catch (Throwable ex) { closeEntityManagerAfterFailedBegin(txObject); throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex); } }
The transactional resource when using JPA is actually the entity manager (underlying implementation is session in hibernate) as you can see and this is the first thing this method does
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
So definitely an entity manager / session is created. The transaction attributes are then passed to the underlying JpaDialect (HibernateJpaDialect) via TransactionDefinition. This class in turn actually gets the underlying Hibernate Session and the transaction API of session.
HibernateJpaDialect { ........ public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) Session session = getSession(entityManager); entityManager.getTransaction().begin(); ...... ...... } ......
Answers 3
First of all, as you use hibernate behind JPA API I will use the term EntityManager
instead of session (strictly the same thing, just a matter of terminology).
Every access to the database using JPA will involve an EntityManager
, you are fetching entities, you need an EntityManager
(EM). What's called 1st level cache is nothing more than the EM managed entities state.
Theoretically the lifecycle of the EM is short and bound to a unit of work (and so generally to a transaction, see Struggling to understand EntityManager proper use).
Now JPA can be used in different way : Container-Managed or User-Managed persistence. When the EM is managed by the container (your case, here spring is the container) this last is in charge of managing the EM scope / lifecycle (create, flush and destroy it for you). As the EM is bounded to a transaction / Unit of Work, this task is delegated to the TransactionManager
(the object handling the @Transactional
annotations).
When you annotate a method using @Transactional(propagation = Propagation.NEVER)
, you are creating a spring logical transaction scope which will ensure that there is no existing underlying JDBC transaction bound to an eventual existing EM, which will not create one and will use JDBC autocommit mode but which will create an EM for this logical transaction scope if none already exists.
Regarding the fact that a new EM instance is created for each DAO call when no transaction logical scope is defined, you have to remember that you cannot access database using JPA outside of the EM. AFAIK hibernate used to throw a no session bound to thread
error in this case but this may have evolved with later releases, otherwise your DAO may be annotated with @Transactional(propagation = Propagation.SUPPORT)
which would also automatically create an EM if no enclosing logical scope exists. This is a bad practice as transaction should be defined at the unit of work, eg. the service level and not the DAO one.
Answers 4
I don't think this is correct behavior. It is true what the colleagues are saying that even without transaction the hibernate is creating a session. But this mean that we are facing two sessions S1 and S2 for the two separate reads from the DAO. At the same time L1 cache is always per session, so it does not make sense for two separate sessions to have a hit for the L1 cache. It seems like your Spring is not respecting the @Transactional(propagation = Propagation.NEVER)
The @Transactional(propagation = Propagation.NEVER) should be equivalent to if you just initialize your service from a main method and do the subsequent calls to the DAO yourself.
Try it in a main class and see how it will react. I doubt it will hit the L1 cache again.
Also I will copy paste the doc from Sprint on propagation NEVER:
NEVER Execute non-transactionally, throw an exception if a transaction exists.
One more question - Is the hibernate configured to AutoCommit. Is it possible that the "runInTransaction" - method is not committing ?
Are you looking to earn cash from your visitors by popunder advertisments?
ReplyDeleteIn case you do, did you take a look at Clicksor?