This is a big topic. The Spring reference doc devotes multiple chapters to it. I recommend reading the ones on Aspect-Oriented Programming and Transactions, as Spring's declarative transaction support uses AOP at its foundation.
But at a very high level, Spring creates proxies for classes that declare @Transactional
on the class itself or on members. The proxy is mostly invisible at runtime. It provides a way for Spring to inject behaviors before, after, or around method calls into the object being proxied. Transaction management is just one example of the behaviors that can be hooked in. Security checks are another. And you can provide your own, too, for things like logging. So when you annotate a method with @Transactional
, Spring dynamically creates a proxy that implements the same interface(s) as the class you're annotating. And when clients make calls into your object, the calls are intercepted and the behaviors injected via the proxy mechanism.
Transactions in EJB work similarly, by the way.
As you observed, through, the proxy mechanism only works when calls come in from some external object. When you make an internal call within the object, you're really making a call through the this
reference, which bypasses the proxy. There are ways of working around that problem, however. I explain one approach in this forum post in which I use a BeanFactoryPostProcessor
to inject an instance of the proxy into "self-referencing" classes at runtime. I save this reference to a member variable called me
. Then if I need to make internal calls that require a change in the transaction status of the thread, I direct the call through the proxy (e.g. me.someMethod()
.) The forum post explains in more detail.
Note that the BeanFactoryPostProcessor
code would be a little different now, as it was written back in the Spring 1.x timeframe. But hopefully it gives you an idea. I have an updated version that I could probably make available.
First of all, since Spring doesn't do persistence itself, it cannot specify what readOnly
should exactly mean. This attribute is only a hint to the provider, the behavior depends on, in this case, Hibernate.
If you specify readOnly
as true
, the flush mode will be set as FlushMode.NEVER
in the current Hibernate Session preventing the session from committing the transaction.
Furthermore, setReadOnly(true) will be called on the JDBC Connection, which is also a hint to the underlying database. If your database supports it (most likely it does), this has basically the same effect as FlushMode.NEVER
, but it's stronger since you cannot even flush manually.
Now let's see how transaction propagation works.
If you don't explicitly set readOnly
to true
, you will have read/write transactions. Depending on the transaction attributes (like REQUIRES_NEW
), sometimes your transaction is suspended at some point, a new one is started and eventually committed, and after that the first transaction is resumed.
OK, we're almost there. Let's see what brings readOnly
into this scenario.
If a method in a read/write transaction calls a method that requires a readOnly transaction, the first one should be suspended, because otherwise a flush/commit would happen at the end of the second method.
Conversely, if you call a method from within a readOnly transaction that requires read/write, again, the first one will be suspended, since it cannot be flushed/committed, and the second method needs that.
In the readOnly-to-readOnly, and the read/write-to-read/write cases the outer transaction doesn't need to be suspended (unless you specify propagation otherwise, obviously).
Best Answer
Good question, although not a trivial one to answer.
Propagation
Defines how transactions relate to each other. Common options:
REQUIRED
: Code will always run in a transaction. Creates a new transaction or reuses one if available.REQUIRES_NEW
: Code will always run in a new transaction. Suspends the current transaction if one exists.The default value for
@Transactional
isREQUIRED
, and this is often what you want.Isolation
Defines the data contract between transactions.
ISOLATION_READ_UNCOMMITTED
: Allows dirty reads.ISOLATION_READ_COMMITTED
: Does not allow dirty reads.ISOLATION_REPEATABLE_READ
: If a row is read twice in the same transaction, the result will always be the same.ISOLATION_SERIALIZABLE
: Performs all transactions in a sequence.The different levels have different performance characteristics in a multi-threaded application. I think if you understand the dirty reads concept you will be able to select a good option.
Defaults may vary between difference databases. As an example, for MariaDB it is
REPEATABLE READ
.Example of when a dirty read can occur:
So a sane default (if such can be claimed) could be
ISOLATION_READ_COMMITTED
, which only lets you read values which have already been committed by other running transactions, in combination with a propagation level ofREQUIRED
. Then you can work from there if your application has other needs.A practical example of where a new transaction will always be created when entering the
provideService
routine and completed when leaving:Had we instead used
REQUIRED
, the transaction would remain open if the transaction was already open when entering the routine. Note also that the result of arollback
could be different as several executions could take part in the same transaction.We can easily verify the behaviour with a test and see how results differ with propagation levels:
With a propagation level of
REQUIRES_NEW
: we would expectfooService.provideService()
was NOT rolled back since it created it's own sub-transaction.REQUIRED
: we would expect everything was rolled back and the backing store was unchanged.