Spring @Transactional read-only propagation
Asked Answered
R

4

102

I'm experimenting with using the command pattern to allow my web layer to work with Hibernate entities within the context of a single transaction (thus avoiding lazy loading exceptions). I am, however, confused now with how I should deal with transactions.

My commands call service layer methods that are annotated with @Transactional annotations. Some of these service layer methods are read-only - e.g. @Transactional(readOnly = true) - and some are read/write.

My service layer exposes a command handler that executes commands passed to it on behalf of the web layer.

@Transactional
public Command handle(Command cmd) throws CommandException

I assume I am right in making the command handler's handle method transactional. This is where the confusion comes in. If the implementation of a command makes calls to multiple service layer methods, there is no way for the command handler to know whether operations called within the command will be read-only, read/write or a combination of the two.

I don't understand how propagation works in this example. If I were to make the handle() method readOnly = true, then what happens if the command then calls a service layer method that is annotated with @Transactional(realOnly = false)?

Reviel answered 23/10, 2009 at 15:14 Comment(2)
So which of the both contradictory answers is true? Has anyone bothered to check?Meantime
Since handle() may call methods which write, the transaction must allow writes. That would be fine & correct as a solution. If you really wanted, you could investigate starting the TX programmatically & switching readOnly -- perhaps via an attribute of Command -- but I seriously doubt it's worth the effort.Coiffeur
S
124

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).

Snug answered 11/11, 2009 at 0:58 Comment(8)
Are you sure ? Will "read-only" really override the specified propagation policy ? I had a hard time finding references but foud at least this post that states the opposite: imranbohoran.blogspot.ch/2011/01/…Instrumental
If you call a bean that has read-only, and then this bean calls another bean with read-write, a new transaction is not started, the second bean participates in the existing read-only transaction, and the changes that the second bean makes are not committed.Particularize
Incorrect -- as @dancarter says, a read/write method called within a readOnly transaction will silently fail to commit, at least with Spring's Hibernate integration. Since the outermost TX interceptor is read-only, the Hibernate session is never flushed.. and no SQL updates are executed. (That's with default propagation attributes -- you could try REQUIRES_NEW, but it's not the right solution for most scenarios.)Coiffeur
@dancarter That's correct. However, if we call a read/write transaction inside a read-only transaction then we are doing something wrong. The opposite, however, is okay.Gold
Also you're totally wrong here "since Spring doesn't do persistence itself, it cannot specify what readOnly should exactly mean. ". In fact spring is the transaction manager, so spring can and does does specify exactly what readOnly means. The @Transactional annotation itself is a spring annotation. Spring as the API provider and implementer gets to specify exactly what that API means.Particularize
You've corrected the answer somewhat, but i still find it vague and misleading. "Conversely, if you call a method from within a readOnly transaction that requires read/write, again, the first one will be suspended, " No, the second read/write annotated method will participate in the existing read-only transaction. Transactions are not automatically suspended. They only suspend if you tell spring to suspend them by using REQUIRES_NEWParticularize
102 upvotes for a wrong answer. I'm glad I read the comments. @dancarter maybe you could edit the part of the answer which is wrong so people don't need read through all the comments? I'm not sure how situations like this should be handled on stackoverflow.Transcendentalistic
I began to read this answer, voted up, and then I've started reading this comments, which show that the provided answer is anything but perfect. So I decided to withdraw my vode, but Stack Overflow doesn't let me do, because of a silly 6 minute rule. Who the heck is responsible for such a bs? However, please read answer below by dan carter.Marcy
P
43

Calling readOnly=false from readOnly=true doesn't work since the previous transaction continues.

In your example, the handle() method on your service layer is starting a new read-write transaction. If the handle method in turn calls service methods that annotated read-only, the read-only will take no effect as they will participate in the existing read-write transaction instead.

If it is essential for those methods to be read-only, then you can annotate them with Propagation.REQUIRES_NEW, and they will then start a new read-only transaction rather than participate in the existing read-write transaction.

Here is a worked example, CircuitStateRepository is a spring-data JPA repository.

BeanS calls a transactional=read-only Bean1, which does a lookup and calls transactional=read-write Bean2 which saves a new object.

  • Bean1 starts a read-only tx.

31 09:39:44.199 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [nz.co.vodafone.wcim.business.Bean1.startSomething]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''

  • Bean 2 pariticipates in it.

    31 09:39:44.230 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - Participating in existing transaction

    Nothing is committed to the database.

Now change Bean2 @Transactional annotation to add propagation=Propagation.REQUIRES_NEW

  • Bean1 starts a read-only tx.

    31 09:31:36.418 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [nz.co.vodafone.wcim.business.Bean1.startSomething]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''

  • Bean2 starts a new read-write tx

    31 09:31:36.449 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - Suspending current transaction, creating new transaction with name [nz.co.vodafone.wcim.business.Bean2.createSomething]

And the changes made by Bean2 are now committed to the database.

Here's the example, tested with spring-data, hibernate and oracle.

@Named
public class BeanS {    
    @Inject
    Bean1 bean1;

    @Scheduled(fixedRate = 20000)
    public void runSomething() {
        bean1.startSomething();
    }
}

@Named
@Transactional(readOnly = true)
public class Bean1 {    
    Logger log = LoggerFactory.getLogger(Bean1.class);

    @Inject
    private CircuitStateRepository csr;

    @Inject
    private Bean2 bean2;

    public void startSomething() {    
        Iterable<CircuitState> s = csr.findAll();
        CircuitState c = s.iterator().next();
        log.info("GOT CIRCUIT {}", c.getCircuitId());
        bean2.createSomething(c.getCircuitId());    
    }
}

@Named
@Transactional(readOnly = false)
public class Bean2 {    
    @Inject
    CircuitStateRepository csr;

    public void createSomething(String circuitId) {
        CircuitState c = new CircuitState(circuitId + "-New-" + new DateTime().toString("hhmmss"), new DateTime());

        csr.save(c);
     }
}
Particularize answered 30/10, 2013 at 20:35 Comment(1)
Amazing answer, but think you should summarise as below because the answer formatting cause some confusion initially. 1. Calling readOnly=false from readOnly=true doesn't work since the previous transaction continues. 2. Calling (propagation = Propagation.REQUIRES_NEW) from readOnly=true WORKS since a new transaction has been created.Truman
B
16

By default transaction propagation is REQUIRED, meaning that the same transaction will propagate from a transactional caller to transactional callee. In this case also the read-only status will propagate. E.g. if a read-only transaction will call a read-write transaction, the whole transaction will be read-only.

Could you use the Open Session in View pattern to allow lazy loading? That way your handle method does not need to be transactional at all.

Berk answered 2/1, 2010 at 23:41 Comment(2)
As @Berk says, read-only status propagates inwards -- without warnings or any diagnosis as to why Hibernate doesn't commit :(Coiffeur
I am using jpa+hibernate+spring, and in the case where a readonly transaction called a readwrite transaction and all the action was in the readwrite transaction, entities that were persisted was committed but entities that were altered through getters/setters did not get committed. Quite confusing.Mann
C
6

It seem to ignore the settings for the current active transaction, it only apply settings to a new transaction:

org.springframework.transaction.PlatformTransactionManager
TransactionStatus getTransaction(TransactionDefinition definition)
                         throws TransactionException
Return a currently active transaction or create a new one, according to the specified propagation behavior.
Note that parameters like isolation level or timeout will only be applied to new transactions, and thus be ignored when participating in active ones.
Furthermore, not all transaction definition settings will be supported by every transaction manager: A proper transaction manager implementation should throw an exception when unsupported settings are encountered.
An exception to the above rule is the read-only flag, which should be ignored if no explicit read-only mode is supported. Essentially, the read-only flag is just a hint for potential optimization.
Cree answered 22/11, 2013 at 0:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.