Phantom read happens when data is read more times and unexpectedly goes away or when data is read more times and unexpectedly a different value is shown. For example, in transaction A, through a query you get a set result. Before transaction A is completed, transaction B adds a row that matches the query executed in transaction A, and this row is committed before the completion of transaction A. If transaction A executes the same query again, it will get a different data. This situation is called phantom read because new data not created by transaction A has appeared in the resource. To simulate this read, we must execute a query. Consider the named query of the account entity:
@Entity
@NamedQueries({ @NamedQuery(name = "SelectAll", query = "SELECT e FROM Account e") })
public class Account {...}
Here is a runnable to execute the query in the first transaction:
@Transactional(value = REQUIRES_NEW)
public class QueryReadAccount implements Runnable {
@Override
public void run() {
...
CountDownLatch latch = new CountDownLatch(1);
latch.await(firstWaitTime, MILLISECONDS);
firstResult = entityManager.createNamedQuery("SelectAll").getResultList().size(); latch.await(secondWaitTime, MILLISECONDS);
secondResult = entityManager.createNamedQuery("SelectAll").getResultList().size();
...
}
}
Here is a runnable that inserts a new account in the second transaction:
@Transactional(value = REQUIRES_NEW)
public class QueryWriteAccount implements Runnable {
@Override
public void run() {
...
CountDownLatch latch = new CountDownLatch(1);
Account account = new Account();
account.setNumber(accountNumber);
account.add(amount);
entityManager.persist(account);
latch.await(waitTime, MILLISECONDS);
result = account.getCredit();
...
}
}
Now, start the two transactions with the managed executor service:
@Inject
private QueryReadAccount queryReadAccount;
@Inject
private QueryWriteAccount queryWriteAccount;
...
queryReadAccount.setFirstWaitTime(0);
queryReadAccount.setSecondWaitTime(2000);
queryWriteAccount.setAccountNumber(953);
queryWriteAccount.setAmount(456.77);
queryWriteAccount.setWaitTime(1000);
defaultExecutor.submit(queryWriteAccount);
defaultExecutor.submit(queryReadAccount);
The operations can be simplified so:
- The first transaction starts and we have a first query of all accounts
- The second transaction starts and an insert of a new account is done
- The second transaction ends
- A second read of all accounts is done in the first transaction
- The first transaction ends
The results of these operations change according the configured transaction isolation level that we will introduce in the next section.