In Spring, we use @EnableTransactionManagement
on main class to enable transactions. In Spring Boot, it is enabled by default so no need for that annotation.
// To make a class or a method transactional
@Transactional
// if applied on a class, it adds the annotation to all public methods and ignore all others
// if applied on a method, if the method is not public, it is ignored
Make sure it is imported from org.springbootframework.transaction
and not javax.transaction
.
@Transactional(rollbackFor = IOException.class)
class Foobar{
@Transactional(rollbackFor = SQLException.class) // override
public void foo(){ }
}
// override order: interface, superclass, class, interface method, superclass method, and class method
// default behaviour is that it rolls back only for unchecked, RuntimeExceptions
@Transactional(rollbackFor = { SQLException.class }) // noRollbackFor
@Transactional(isolation = Isolation.READ_UNCOMMITED)
@Transactional(readOnly = true) // serves as a hint that transaction doesn't perform any insert or updates; won't cause exceptions, just a hint so it doesn't lock resources
@Transactional(timeout = 5) // transaction timeout in seconds
@Transactional(propagation = Propagation.REQUIRES_NEW) // creates new transaction for each child method; REQUIRED is default
// others: REQUIRES_NEW, NESTED, MANDATORY, etc...
// logging
loggging.level.org.springframework.transaction=TRACE
Any methods that are called inside an annotated method will be covered in a single transaction, this is called an “existing” transaction. This behaviour can be changed with propagation=...
property in the annotation.
REQUIRED
(default) - if an existing transaction is active, use that; otherwise create a new oneREQUIRES_NEW
- create a new transaction regardless of existing one; suspend any active existing transactionNESTED
- mark a savepoint here in the existing active transaction; if an active transaction doesn’t exist, it works like REQUIREDMANDATORY
- if an active transaction exists, use it; otherwise throw an exceptionAs discussed in the below section, @Transactional on any method is ignored by Spring if the call isn’t coming through the proxy.
All child methods of a method annotated (and called via proxy) will be covered under transaction, even non-public ones:
class Foo{
@Transactional
public void foo(){
bar(); // method call
}
private void bar(){ } // will be covered under transaction
}
// foo() also must be called from outside the class or must have an existing active transaction
Transactional on any non-public method is ignored and won’t create a new transaction but will use the existing active one, so we can’t create smaller independent transactions for non-public methods:
class Foo{
@Transactional
public void foo(){
bar(); // method call
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // ignored; without compiler-errors
private void bar(){ } // will be covered under existing transaction
}
@Transactional is only valid for calls to public method coming from another class only. Any public
method called from within the class ignores the @Transactional on it too:
class Foo{
@Transactional
public void foo(){
bar(); // method call
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // ignored if called from foo(); works when called from outside class Foo
public void bar(){ }
}
Exception Propagation: Declare checked exceptions as usual. If a method propagates an exception (doesn’t handle it), rollback will happen for it if rollbackFor
property specifies it. Rollback will happen for every method the exception propagates to, until its handled.
class Foo{
@Transactional(rollbackFor = SQLException.class) // rolls back; because of exception propagation; not in the same transaction as bar()
public void foo() throws SQLException {
bar.fun2(); // external method call; unhandled exception
}
}
// another class
class Bar{
@Transactional(rollbackFor = SQLException.class, propagation = Propagation.REQUIRES_NEW) // new transaction; rolls back
public void fun2() throws SQLException{
throw new SQLException(); // exception occurs here
}
}
Summary: All method calls coming from within the same class ignore @Transactional annotation on that method, no matter if the method is non-public or public.
Spring creates another bean (proxy) for the class having @Transactional
annotation methods (target object). It replaces the original bean in the application context and wraps the transactional logic around method invocations.
The proxy intercepts method calls from outside the target object and applies the advice (i.e. transactional logic) before delegating the call to the actual method. This means that only external calls to methods that come in through the proxy are covered under transactions, any internal method calls bypass the proxy even if annotated because they are direct calls within the same object instance!. We can cover such private
methods programmatically using PlatformTransactionManager
and manually commiting or rolling back.
Programmative Way using PlatformTransactionManager
:
@Autowired
private PlatformTransactionManager transactionManager;
// inside the method
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can only be done programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// execute your business logic here
}
catch (MyException ex) {
transactionManager.rollback(status);
throw ex;
}
transactionManager.commit(status);
@Transactional(isolation = Isolation.DEFAULT) // use DB provider's default level
// READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
Explanations: Isolation Notes
By default spring-boot-starter-cache
will include support for EhCache. To be able to use Redis we need to add custom config class (1) or add spring-boot-starter-data-redis
and then use properties file to customize it (2). For Ignite we can create a custom config.
# Redis cache config
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
Add @EnableCaching
on main application class to enable caching in Spring Boot.
// specify cache properties for whole class
@CacheConfig(cacheNames = "students")
// on methods
@Cacheable(cacheNames = "students") // read
@CachePut // insert
@CacheEvict // delete
@Caching(evict = {@CacheEvict("phone_number"), @CacheEvict(value = "directory", key = "#student.id") }) // using same annotation multiple times
@Cacheable(value = "students", key = "#id") // notice that value is not "key's value" but alias for "cacheNames" property only
Student getStudentName(Long id){ }
@Cacheable(value = "students", key = "#s.id") // notice the expression
Long getStudentId(Student s){ }
@CacheEvict(value = "student", allEntries=true) // evict all entries from cache
@Cacheable(value = "student", sync=true) // sync the underlying method (for multi-threading)
// condition on input
@CachePut(value="addresses", condition = "#customer.name == 'Tom'")
public String getAddress(Customer customer) { }
// condition on output
@CachePut(value="addresses", unless = "#result.length() < 64")
public String getAddress(Customer customer) { }
Spring will take care of storing in cache based on cache provider. Ex - In the above example, we specify cacheName
as “students” and Redis won’t neccessarily use the same name as-it-is to create keys, the name is often a combination of cacheName
and various other stuff like student name and other fields whose fetching and decoding is taken care of implicitly.