Enterprise Java Development@TOPIC@
In this chapter we will take a brief look at how you can incorporate database locks into your queries to help address race conditions. We will not be covering JPA locking in detail here. We will be limiting our coverage to how to integrate queries with JPA locks.
In this section you will setup a JUnit test case to do work within this chapter.
Create a JUnit test case in src/test called QueryLocksTest. Have this class extend QueryBase. Create an initial test method to verify the setup/teardown works correctly.
package myorg.queryex;
import static org.junit.Assert.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
public class QueryLocksTest extends QueryBase {
private static final Log log = LogFactory.getLog(QueryLocksTest.class);
@Test
public void test(){}
}
Build the module and run the test case.
$ mvn clean test -P\!h2db -Ph2srv -Dtest=myorg.queryex.QueryLocksTest ... Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
You may remove the sample @Test at this time since we will be adding more tests below.
For this topic -- we will be looking at the case of a database writer that needs to either update or insert depending on the results of a query. To make this realistic, we will run the same code in multiple threads and purposely form a race condition where we hope to leverage locks to provide us one INSERT and multiple UPDATEs.
Add the following JUnit @Before method to your test case. This will perform the cleanup/populate in between each test method since test methods in this test case change the database. The parent class will take care of cleanup/populate prior to running the next test case.
@Before
public void setUpLocksTest() {
em.getTransaction().commit();
cleanup(em);
populate(em);
}
Add the following enum to your test case. This will be used by the Writer objects to tell the main loop what they did individually to the database.
public static enum Action { INSERT, UPDATE, FAIL };
Add the following Writer class. This class will be used to perform a transaction with the database within its own Java Thread. The transaction is started during the constructor and finished during the run method.
private class Writer extends Thread {
private String context;
private Actor actor;
private LockModeType lockMode;
private EntityManager em_;
private Action action;
private int sleepTime=100;
private String errorText;
public Writer(String context, Actor actor, LockModeType lockMode) {
this.context = context;
this.actor = actor;
this.lockMode = lockMode;
em_ = emf.createEntityManager();
em_.getTransaction().begin();
log.debug(context + " transaction started");
}
public boolean isDone() { return action != null && em_==null; }
public String getContext() { return context; }
public Action getAction() { return action; }
public String getErrorText() { return errorText; }
public void run() {
//...
}
};
Implement the run() method for the Writer class. The method will search for the entity in the database and either create or update it depending on the result of the query.
public void run() {
try {
log.debug(context + " selecting with lockMode=" + lockMode);
List<Actor> actors = em_.createQuery(
"select a from Actor a JOIN a.person as p " +
"where p.firstName=:firstName and p.lastName=:lastName " +
"or p.firstName='" + context + "'", Actor.class)
.setLockMode(lockMode)
.setParameter("firstName", actor.getFirstName())
.setParameter("lastName", actor.getLastName())
.setMaxResults(1)
.getResultList();
Notice...
We are passing in a lockMode property into the query above.
We are performing an INSERT if nothing is returned -- else UPDATE
try {
log.debug(context + " sleeping " + sleepTime + " msecs");
Thread.sleep(sleepTime);
} catch (Exception ex){}
if (actors.size()==0) {
log.debug(context + " creating entity");
em_.persist(actor);
action=Action.INSERT;
} else {
log.debug(context + " updating entity");
actors.get(0).setBirthDate(actor.getBirthDate());
action=Action.UPDATE;
}
We finish up the method with a commit/rollback of the transaction and general accounting so the main loop will know what this instance did.
em_.flush();
log.debug(context + " committing transaction version=" + actor.getVersion());
em_.getTransaction().commit();
log.debug(context + " committed transaction version=" + actor.getVersion());
} catch (PersistenceException ex) {
log.debug(context + " failed " + ex);
em_.getTransaction().rollback();
action = Action.FAIL; errorText = ex.toString();
} finally {
em_.close(); em_=null;
}
}
Add a helper method to setup and execute each test. This helper will accept a LockModeType and count of threads to execute. The helper method will supply each thread with an instance to either INSERT or UPDATE. The primary key is unique -- so the thread will use a query based on the properties of the object.
protected int testUpsert(LockModeType lockMode, int count) {
List<Writer> writers = new ArrayList<QueryLocksTest.Writer>();
//create writer instances within their own thread
for (int i=0; i<count; i++) {
Date birthDate = new GregorianCalendar(1969+i, Calendar.MAY, 25).getTime();
Actor actor = new Actor(new Person("test-actor" + i)
.setFirstName("Anne")
.setLastName("Heche")
.setBirthDate(birthDate));
writers.add(new Writer("writer" + i, actor, lockMode));
}
//...
}
This portion of the helper method will cause the following output from each thread.
-writer0 transaction started
Add the following lines to the helper method to start each thread.
//start each of the threads
List<Writer> working = new ArrayList<Writer>();
for (Writer writer : writers) {
working.add(writer); writer.start();
}
This will produce the following output out of each thread. However, the queries will differ slightly depending on the LockModeType used.
-writer0 selecting with lockMode=NONE Hibernate: select actor0_.PERSON_ID as PERSON1_1_, actor0_.version as version1_ from QUERYEX_ACTOR actor0_ inner join QUERYEX_PERSON person1_ on actor0_.PERSON_ID=person1_.ID where person1_.FIRST_NAME=? and person1_.LAST_NAME=? or person1_.FIRST_NAME='writer0' limit ? -writer0 sleeping 100 msecs
The above code will also cause the following to be produced when no match is found by the query.
-writer0 creating entity Hibernate: insert into QUERYEX_PERSON (BIRTH_DATE, FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?) Hibernate: insert into QUERYEX_ACTOR (version, PERSON_ID) values (?, ?)
The threads have been designed to delay processing between the select and the INSERT/UPDATE to simulate additional work and to cause neighboring threads to wait (if configured to do so).
-writer0 committing transaction version=0 -writer0 committed transaction version=0
Add the following lines to the helper method to wait for the threads to complete.
//run until all writers complete
while (!working.isEmpty()) {
try { Thread.sleep(100); } catch (Exception ex) {}
Iterator<Writer> itr = working.iterator();
while (itr.hasNext()) {
if (itr.next().isDone()) { itr.remove(); }
}
}
Add the following lines to the helper method to query for the results and log them. Notice the use of a JOIN FETCH in the query to assure the query performs an EAGER fetch of Person in the same query as the Actor.
//get the resultant entries in database
List<Actor> actors = em.createQuery(
"select a from Actor a JOIN FETCH a.person as p " +
"where p.firstName=:firstName and p.lastName=:lastName", Actor.class)
.setParameter("firstName", "Anne")
.setParameter("lastName", "Heche")
.getResultList();
log.debug("actors=" + actors);
for (Writer w : writers) {
log.debug(String.format("%s => %s %s", w.getContext(), w.getAction(), w.getErrorText()==null?"":w.getErrorText()));
}
This will produce the following output during the test method.
Hibernate: select actor0_.PERSON_ID as PERSON1_1_0_, person1_.ID as ID0_1_, actor0_.version as version1_0_, person1_.BIRTH_DATE as BIRTH2_0_1_, person1_.FIRST_NAME as FIRST3_0_1_, person1_.LAST_NAME as LAST4_0_1_ from QUERYEX_ACTOR actor0_ inner join QUERYEX_PERSON person1_ on actor0_.PERSON_ID=person1_.ID where person1_.FIRST_NAME=? and person1_.LAST_NAME=? -actors=[Anne Heche, version=0] -writer0 => INSERT
Notice there is an audit of what each of the threads performed (INSERT or UPDATE) at the end of the above output.
Add the following line to the helper method to return the number of rows found in the database.
return actors.size();
Add the following test method to verify the code added above and the general working of the test case.
@Test
public void testSimple() {
log.info("*** testPersistentSimple ***");
assertEquals("unexpected number of actors", 1, testUpsert(LockModeType.NONE, 1));
}
Run the simple test method to verify the functionality of the test case. Since we use a single thread and no locking, the output is pretty straight forward.
$ mvn clean test -Dtest=myorg.queryex.QueryLocksTest#testSimple ... -*** testPersistentSimple *** -writer0 transaction started -writer0 selecting with lockMode=NONE Hibernate: select actor0_.PERSON_ID as PERSON1_1_, actor0_.version as version1_ from QUERYEX_ACTOR actor0_ inner join QUERYEX_PERSON person1_ on actor0_.PERSON_ID=person1_.ID where person1_.FIRST_NAME=? and person1_.LAST_NAME=? or person1_.FIRST_NAME='writer0' limit ? -writer0 creating entity Hibernate: insert into QUERYEX_PERSON (BIRTH_DATE, FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?) Hibernate: insert into QUERYEX_ACTOR (version, PERSON_ID) values (?, ?) -writer0 sleeping 100 msecs -writer0 committing transaction version=0 -writer0 committed transaction version=0 Hibernate: select actor0_.PERSON_ID as PERSON1_1_0_, person1_.ID as ID0_1_, actor0_.version as version1_0_, person1_.BIRTH_DATE as BIRTH2_0_1_, person1_.FIRST_NAME as FIRST3_0_1_, person1_.LAST_NAME as LAST4_0_1_ from QUERYEX_ACTOR actor0_ inner join QUERYEX_PERSON person1_ on actor0_.PERSON_ID=person1_.ID where person1_.FIRST_NAME=? and person1_.LAST_NAME=? -actors=[Anne Heche, version=0] -writer0 => INSERT ... [INFO] BUILD SUCCESS
In this first section we will demonstrate the problem of implementing INSERT/UPDATE without the ability to form a database lock within the initial query. Without a lock -- the results of the initial query become invalidated by the time this transaction completes and we do not get the desired results.
Add the following test method to your existing test case. This will add additional threads to the simple test case run earlier.
@Test
public void testNONE() {
log.info("*** testNONE ***");
int count=testUpsert(LockModeType.NONE, 5);
for (int i=0; i<10 && count<=1; i++) {
//can't always trigger race condition -- so retry
cleanup(em);
populate(em);
count=testUpsert(LockModeType.NONE, 5);
}
assertTrue("unexpected number of actors", count > 1);
}
Due to the unpredictability of the race condition -- we may have to run the test more than once.
Run the new test method and observe the final results. Although each execution will be slightly different -- the issue is we get more than a single thread creating an instance of the entity. You will find all INSERTS got their query results first and all UPDATES go their results last.
$ mvn clean test -Dtest=myorg.queryex.QueryLocksTest#testNONE ... -writer0 => INSERT -writer1 => UPDATE -writer2 => UPDATE -writer3 => UPDATE -writer4 => INSERT ... [INFO] BUILD SUCCESS
In this section we will drop back to a single thread but add LockMode to the query so the difference can be easily seen.
Add the following test method to your existing test case.
@Test
public void testPessimisticWrite1() {
log.info("*** testPersistentWrite1 ***");
assertEquals("unexpected number of actors", 1, testUpsert(LockModeType.PESSIMISTIC_WRITE, 1));
}
Run the new test method and note the difference in the query output. The provider has added a "for update" at the end of the query. This will form a lock in the database. For the H2 database we are using -- this is a table lock.
$ mvn clean test -Dtest=myorg.queryex.QueryLocksTest#testPessimisticWrite1 ... -*** testPersistentWrite1 *** -writer0 transaction started -writer0 selecting with lockMode=PESSIMISTIC_WRITE Hibernate: select actor0_.PERSON_ID as PERSON1_1_, actor0_.version as version1_ from QUERYEX_ACTOR actor0_ inner join QUERYEX_PERSON person1_ on actor0_.PERSON_ID=person1_.ID where person1_.FIRST_NAME=? and person1_.LAST_NAME=? or person1_.FIRST_NAME='writer0' limit ? for update ... -writer0 => INSERT ... [INFO] BUILD SUCCESS
In this section we will add several threads -- all using PESSIMISTIC_WRITE locks.
Add the following test method to your existing test case. This is the same LockMode as before except we have added additional threads.
@Test
public void testPessimisticWrite() {
log.info("*** testPersistentWrite ***");
assertEquals("unexpected number of actors", 1, testUpsert(LockModeType.PESSIMISTIC_WRITE, 5));
}
Run the new test method and notice we get a single INSERT and multiple UPDATEs every time the test is run. That is because all subsequent selects are blocked until the first select commit()s its transaction.
$ mvn clean test -Dtest=myorg.queryex.QueryLocksTest#testPessimisticWrite ... -writer0 => INSERT -writer1 => UPDATE -writer2 => UPDATE -writer3 => UPDATE -writer4 => UPDATE ... [INFO] BUILD SUCCESS
This chapter exposed at least one problem that can be corrected using pessimistic locking and lockMode within the JPA query. The details of locking are outside the scope of this chapter and is also subject to database capability and connection modes selected. We want to leave you with just a taste of what you can do and how to express that within a JPA query.