I'm playing with Spring and the @Transactional
annotation. I'm doing a simple experiment to test the behaviour of this annotation. These are my super simple java classes:
my domain:
package com.xxx.springdemo.transactionalAnnotation.domain;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
@Data
@Entity
public class Counter {
@Id
int id;
int count = 0;
}
my service:
package com.xxx.springdemo.transactionalAnnotation.services;
import com.xxx.springdemo.transactionalAnnotation.domain.Counter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class TransactionalService {
public void throwException(Counter counter) {
counter.setCount(1);
throw new RuntimeException("Runtime exception");
}
public void dontThrowException(Counter counter) {
counter.setCount(2);
}
}
and my controller:
package com.xxx.springdemo.transactionalAnnotation.controllers;
import com.xxx.springdemo.transactionalAnnotation.domain.Counter;
import com.xxx.springdemo.transactionalAnnotation.services.TransactionalService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class TransactionController {
private final TransactionalService transactionalService;
@GetMapping("test-transactional")
public void testTransactional() {
Counter counter = new Counter();
try {
transactionalService.throwException(counter);
} catch (Exception e) {
System.out.println("Exception caught: " e.getMessage());
}
System.out.println("Variable hasn't changed (counter = " counter.getCount() ")");
}
@GetMapping("test-transactional-2")
public void testTransactionalWithoutException() {
Counter counter = new Counter();
try {
transactionalService.dontThrowException(counter);
} catch (Exception e) {
System.out.println("Exception caught: " e.getMessage());
}
System.out.println("Variable changed (counter = " counter.getCount() ")");
}
}
The code should be self-explanatory. I expect to have printed:
Variable hasn't changed (counter = 0)
in case the test-transactional
endpoint is called. What I get instead is:
Variable hasn't changed (counter = 1)
This means that in the service method throwException
the count property isn't rollbacked after the exception is thrown.
P.S. this is my build.gradle file:
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
CodePudding user response:
Spring 's @Transactional
is just a declarative way to manage the underlying JPA transaction. So it depends on the rollback behaviour of JPA which is defined by the spec. as follows :
For both transaction-scoped persistence contexts and for extended persistence contexts that are joined to the current transaction, transaction rollback causes all pre-existing managed instances and removed instances[29] to become detached. The instances' state will be the state of the instances at the point at which the transaction was rolled back. Transaction rollback typically causes the persistence context to be in an inconsistent state at the point of rollback.
Hibernate docs also mentions the same :
Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction. This means that the database state and the business objects will be out of sync. Usually, this is not a problem because exceptions are not recoverable and you will have to start over after rollback anyway.
So it is the expected rollback behaviour which the Counter
will still have the state just before the transaction is rollback (i.e count=1
).
For the entity that is already exist in the DB , you can restore its state back to the same as DB manually by using entityManager.refresh()
or simply use entityManager.get()
or JPQL to retrieve it again from the DB.
For the entity that does not exist in the DB , simply re-execute the codes to create it again.
So rollback here just means nothing will be updated in DB. It does not mean that it will restore the object state to the moment just before executing a @Transactional
method.