Can someone explain what isolation & propagation parameters are for in the @Transactional
annotation via real-world example?
Basically when and why I should choose to change their default values.
Good question, although not a trivial one to answer.
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. Defines the data contract between transactions.
Read Uncommitted
: Allows dirty reads.Read Committed
: Does not allow dirty reads.Repeatable Read
: If a row is read twice in the same transaction, the result will always be the same.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.
Example of when a dirty read can occur:
thread 1 thread 2
| |
write(x) |
| |
| read(x)
| |
rollback |
v v
value (x) is now dirty (incorrect)
So a sane default (if such can be claimed) could be Read Committed
, which only lets you read values which have already been committed by other running transactions, in combination with a propagation level of Required
. 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:
public class FooService {
private Repository repo1;
private Repository repo2;
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void provideService() {
repo1.retrieveFoo();
repo2.retrieveFoo();
}
}
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 a rollback
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:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:/fooService.xml")
public class FooServiceTests {
private @Autowired TransactionManager transactionManager;
private @Autowired FooService fooService;
@Test
public void testProvideService() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
fooService.provideService();
transactionManager.rollback(status);
// assert repository values are unchanged ...
}
With a propagation level of
Requires new
: we would expect fooService.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.