0

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
Marinus
  • 21
  • 2
  • [PostgreSQL does not support dirty reads (READ UNCOMMITTED).](https://stackoverflow.com/q/33646012/5221149) – Andreas Jan 30 '20 at 14:23
  • [Oracle Database doesn't use dirty reads, nor does it even allow them.](https://stackoverflow.com/q/208236/5221149) – Andreas Jan 30 '20 at 14:24
  • *"I **also** tested against a Postgres database"* Don't know what database you tested on before Postgres *(kind of an important piece of information, don't you think?)*, but if it was Oracle DB, then you tested on the 2 databases that don't support Read Uncommitted. – Andreas Jan 30 '20 at 14:28
  • This would be a simpler example if you were not also using spring and JPA. They are not necessary for an example and they complicate things. – Deadron Jan 30 '20 at 14:53
  • 1
    You need multiple threads to test dirty reads. The current approach reuses the same underlying thread bound `EntityManager`. At least you need 2 dedicated connections to the database, the current one shares a single one. Finally as mentioned you need a DB that does suppport dirty reads (like H2 for instance). On a different note your `equals` and `hashCode` are flawed for JPA, identity shouldn't change for persisted entities. – M. Deinum Jan 30 '20 at 14:56
  • Thanks for your answers. @Andreas: I forgot to add the pom.xml which would point out that I used a h2. I checked the isolation support for h2, but not for postgres which wasn't very smart. – Marinus Feb 05 '20 at 15:44
  • @M.Deinum: thanks for your answer. I already tried (and retried) using threading to solve this issue based on other articles about this subject. However I am not able to solve it. Can you give me some pointers how I can have a dedicated connection per thread? – Marinus Feb 05 '20 at 15:49
  • @Deadron: I also implemented a solution with java.sql.Connection and jdbcTemplates in combination with a H2 in memory database. However no luck either. :-( – Marinus Feb 05 '20 at 15:51
  • 1
    @Marinus I think you may be having a different issue now. You should create a new ticket. Make sure you check that your database of choice actually supports dirty reads since the previous comments point out postgres does not. – Deadron Feb 05 '20 at 16:06

0 Answers0