views:

321

answers:

4

I'd like to write a unit test to verify that optimistic locking is properly set up (using Spring and Hibernate).

I'd like to have the test class extend Spring's AbstractTransactionalJUnit4SpringContextTests.

What I want to end up with is a method like this:


@Test (expected = StaleObjectStateException.class) 
public void testOptimisticLocking() {
    A a = getCurrentSession().load(A.class, 1);
    a.setVersion(a.getVersion()-1);
    getCurrentSession().saveOrUpdate(a);
    getCurrentSession().flush();           
    fail("Optimistic locking does not work");
}

This test fails. What do you recommend as a best practice?

The reason I am trying to do this is that I want to transfer the version to the client (using a DTO). I want to prove that when the DTO is sent back to the server and merged with a freshly loaded entity, saving that entity will fail if it's been updated by somebody else in the meantime.

+2  A: 

The bit your missing here is

A a1 = getCurrentSession().load(A.class, 1);
A a2 = getCurrentSession().load(A.class, 1);    

System.out.println("the following is true!! " + a1 == a2) ;

Hibernate will return the same instance for the same class/id for the same session.

In order to test the optimistic locking try some thing like:

A a1 = getCurrentSession().load(A.class, 1);

// update the version number in the db using some sql.
runSql("update A set version = version + 1 where id = 1");

// change object
a1.setField1("Thing");

getCurrentSession().flush(); // bang should get exception here
Gareth Davis
Thanks for the answer (+1), this will test optimistic locking. I'm gonna try to update the question though to fully reflect what I'm trying to achieve.
Michal Bachman
no problems... will have a bit later once I'm not a work... already spent too much time on SO today :)
Gareth Davis
A: 

With an AbstractTransactionalDataSourceSpringContextTests, you would do it like this (code taken from this thread):

public void testStatelessDetectedOnObjectWithOptimisticLocking () {
    long id = 1l;
    CoffeeMachine cm1 = (CoffeeMachine) hibernateTemplate.get(CoffeeMachine.class, id);
    Session firstSession = hibernateTemplate.getSessionFactory().getCurrentSession();

    endTransaction();

    // Change outside session
    cm1.setManufacturerName("And now for something completely different");

    startNewTransaction();
    Session secondSession = hibernateTemplate.getSessionFactory().getCurrentSe ssion();
    assertNotSame(firstSession, secondSession);

    CoffeeMachine cm2 = (CoffeeMachine) hibernateTemplate.get(CoffeeMachine.class, id);
    cm2.setManufacturerName("Ha ha! Changed by someone else first. Beat you!");

    hibernateTemplate.flush();

    try {
        hibernateTemplate.merge(cm1);
        fail("Stateless should be detected");
    }
    catch (OptimisticLockingFailureException ex) {
    // OK
    }
}

Note the use of startNewTransaction() (that's the key here).

Pascal Thivent
Thanks, I've seen this but AbstractTransactionalJUnit4SpringContextTests does not have the transaction methods...
Michal Bachman
A: 

It seems that it simply isn't an option to transfer all the fields from a DTO (including version) to a freshly loaded entity, try to save it, and get an exception in case the entity was modified while the DTO was being modified on the client.

The reason for that is that Hibernate simply doesn't care what you do to the version field, given that you're working in the same session. The value of the version field is remembered by the session.

A simple proof of that:


@Test (expected = StaleObjectStateException.class) 
public void testOptimisticLocking() {
    A a = getCurrentSession().load(A.class, 1);
    getCurrentSession().evict(a); //comment this out and the test fails
    a.setVersion(a.getVersion()-1);
    getCurrentSession().saveOrUpdate(a);
    getCurrentSession().flush();           
    fail("Optimistic locking does not work");
}

Thanks everyone for help anyway!

Michal Bachman
A: 

Does the hibernate-controlled version-number optimistic locking feature also work when two seperate hibernate applications are accessing the same database and try to update the same record ? Each application is executing it's transactions using an ISOLATION LEVEL of READ_COMMITTED.

Let's say for example the sequence with the txn is: - Start txn - Select Person from database using a Hibernate HQL query. - Update Person in memory. - Update Person using Hibernate saveOrUpdate. - Commit txn

When Hibernate executes the saveOrUpdate I guess it will execute an update ... set version= ? ... where ..., version = ? sql statement. That is, that sql update includes setting of the incremented version and its where clause uses the old version being the version that was in the record when it was loaded by the initial select HQL query.

Now let's say two different txn's within the SAME hibernate applications are executing that sequence. That is, two different txns are started: Txn1 select the Person (version=0), Txn2 selects the Person (version=0). Txn1 updates the Person in memory, Txn2 updates the person in memory. Txn1 executes the update statement, Txn2 executes the update statement. Txn1 commits, Txn2 commits. At the time Txn1 and Txn2 perform the update the version is still 0 in the database, so Txn1 and Txn2 will increment the version to 1 and will commit successfully and an update will be lost.

Am I right, or will the fact that these txn's are running in the same hibernate application make things work ?

What will be the behaviour if these txn's are executed by TWO DIFFERENT applications accessing the same database ? I guess the behaviour will be the same: no exception, a lost update ?

Am I right ? If so, what can be done about this ? If I'm not right, what am I misunderstanding ?

Kind regards, EDH

EdwinDhondt
@EdwinDhondt: You are almost right, but both txn1 and txn2 will issue "update person set ..., version = 2 where version = 1". So, one transaction will succeed, while another will gently "fail", as DB will report 0 updated rows and Hibernate will raise an exception. So the concurrent problem is solved, but application may still work ineffectively, because it does two jobs twice (and one is thrown away).
dma_k