For teaching purposes I want to provide an example with dirty reads. Using the READ_UNCOMMITED isolation level. However I am not able to get this example working. I tried many different things but it still does not work. So I hope you are able to help me.
Scenario:
- Start transaction “main”
- Insert person in new transaction (“main” transaction is suspended)
- Update name of person in main transaction
- Flush update
- Read person in new transaction (isolation level is READ_UNCOMMITTED)
- Should read data of updated person --> This does not work!
- Revert main transaction by throwing RuntimeException
- Assert that that original name of person is in database --> This works
See code below for more info.
I also tested against a Postgres database but that made no difference. Also tried to do everything with JDBC templates instead of an ORM mapper but that made no difference either.
Thanks in advance.
Best regards, Marinus
IsoliationLevelTest (src/test/java/test)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringBootTestApplication.class})
public class IsoliationLevelTest {
@Autowired
private TestService testService;
@Autowired
private UtilService serviceUtil;
@After
public void tearDown() {
serviceUtil.deleteTestPersons();
}
@Test
public void testIsolationLevel() {
try {
testService.testIsolationLevel("Piet", "PietUpdated");
} catch (TestService.TestException e) {
List<PersonJPAEntity> persons = serviceUtil.retrieveTestPersons();
assertEquals(1, persons.size());
assertEquals("Piet", persons.get(0).getName());
assertEquals("PietUpdated", e.getPersonReadInNewTransaction().getName());
}
}
}
SpringBootTestApplication (src/main/java/test)
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
}
}
TestService
@Service
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public class TestService {
Logger logger = LoggerFactory.getLogger(TestService.class);
@Autowired
private PersonRepository personRepository;
@Transactional(propagation = REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void testIsolationLevel(String initialName, String newName) {
//Requires_new propagation so transaction is committed after person is save
TestService self = (TestService) AopContext.currentProxy();
self.insertPerson(new PersonJPAEntity(1, initialName));
//Required propagation so this update runs in current transaction (which is not committed yet)
self.updatePerson(newName);
PersonJPAEntity personReadInNewTransaction = self.findPersonInNewTransaction();
logger.info("Throw exception and thereby rollback transaction");
throw new TestException("Rollback transaction", personReadInNewTransaction);
}
@Transactional(propagation = REQUIRES_NEW)
public void insertPerson(PersonJPAEntity person) {
personRepository.save(person);
logger.info("Person inserted {}", person);
}
@Transactional(propagation = REQUIRED)
public void updatePerson(String newName) {
Optional<PersonJPAEntity> personToUpdate = personRepository.findById(1);
personToUpdate.get().setName(newName);
logger.info("Person updated {}", personToUpdate);
personRepository.flush();
logger.info("Repository flushed");
}
@Transactional(propagation = REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public PersonJPAEntity findPersonInNewTransaction() {
Optional<PersonJPAEntity> person = personRepository.findById(1);
logger.info("Person found in new transaction {}", person);
return person.get();
}
public class TestException extends RuntimeException {
private final PersonJPAEntity personReadInNewTransaction;
public TestException(String message, PersonJPAEntity personReadInNewTransaction) {
super(message);
this.personReadInNewTransaction = personReadInNewTransaction;
}
public PersonJPAEntity getPersonReadInNewTransaction() {
return personReadInNewTransaction;
}
}
}
PersonRepository
public interface PersonRepository extends JpaRepository<PersonJPAEntity, Integer> {
List<PersonJPAEntity> findByOrderByIdAsc();
}
UtilService
@Service
public class UtilService {
@Autowired
PersonRepository personRepository;
@Transactional(propagation = REQUIRES_NEW)
public List<PersonJPAEntity> retrieveTestPersons() {
return personRepository.findByOrderByIdAsc();
}
@Transactional(propagation = REQUIRES_NEW)
public void deleteTestPersons() {
personRepository.deleteAll();
}
}
PersonJPAEntity (src/main/java/test)
@Entity(name = "person")
public class PersonJPAEntity {
@Id
private int id;
private String name;
private PersonJPAEntity() {
}
PersonJPAEntity(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PersonJPAEntity)) return false;
PersonJPAEntity person = (PersonJPAEntity) o;
return id == person.id &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
application.properties (src/main/resources)
# spring.jpa.show-sql=true
logging.level.org.springframework.transaction=TRACE