Dirty read example using ISOLATION LEVELS

987 views Asked by At

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
0

There are 0 answers