Enterprise Java Development@TOPIC@

Chapter 46. Query Locks

46.1. Setup
46.2. Additional Setup
46.3. Using No Locks
46.4. Adding Lock Mode
46.5. Using Pessimistic Write Lock
46.6. Summary

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.

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.

  1. 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);
    }
  2. 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 };
  3. 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() {
        //...
        }
    };
  4. 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...

    
    
            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;
        }
    }
  5. 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
    
  6. 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
    
  7. 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(); }
        }
    }
  8. 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.

  9. Add the following line to the helper method to return the number of rows found in the database.

    
    
    return actors.size();
  10. 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));
    }
  11. 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.

In this section we will drop back to a single thread but add LockMode to the query so the difference can be easily seen.

In this section we will add several threads -- all using PESSIMISTIC_WRITE locks.

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.