tags:

views:

95

answers:

4

I am in the midst of changing my JPA code around to make use of threads. I have a separate entity manager and transaction for each thread.

What I used to have (for the single threaded environment) was code like:

// get object from the entity manager
X x = getObjectX(jpaQuery);

if(x == null)
{
    x = new X();
    x.setVariable(foo);
    entityManager.persist(x);
}

With that code in the multi threaded environment I am getting duplicate keys since, I assume, getObjectX returns null for a thread, then that thread is swapped out, the next thread calls getObjextX, also getting null, and then both threads will create and persist a new X().

Short of adding in synchronization, is there an atomic way to get/save-if-doesn't-exist a value with JPA or should I rethink my approach

EDIT:

I am using the latest Eclipselink and MySql 5.1

EDIT 2:

I added synchronization... MASSIVE performance hit (to the point that it cannot be used). Going to gather all of the data up back on the main thread and then do the creations on that thread.

+2  A: 

I think you will need to add a unique constraint on the fields that are used in "jpaQuery" so that the database can not create duplicate rows with the same criteria used in the contraints for that query. The calling code will need to trap the resulting exception that occurs as a result of the constraint violation (ideally it will be an EntityExistsException, but the spec is not clear on this case).

Michael Barker
I do have unique constraints, and I am getting the exception. I was hoping to find a way to do it without there being an exception raised...
TofuBeer
Using an exception is probably the simplest solution in this scenario, it's similar to optimistic locking which also uses an exception on failure.
Michael Barker
A: 

Are you sure you need multiple entitymanagers? In a similar situation, I just use one entitymanager and simple per-method lock objects:

private Object customerLock = new Object[0];

public Customer createCustomer(){
    Customer customer = new Customer();
    synchronized(customerLock){
        entityManager.persist(customer);
    }
    return customer;
}

Edit: OK, can't do much about performance except saying that it performs ok in my apps, but for uniqueness use something like this:

public Customer getOrCreateCustomer(String firstName, String lastName){
    synchronized(customerLock){
        List<Customer> customers = 
            entityManager.createQuery(
                "select c from Customer c where c.firstName = :firstName"
                + " and c.lastName = :lastName"
            )
            .setParam("firstName", firstName)
            .setParam("lastName", lastName)
            .setMaxResults(1)
            .getResultList();
        if(customers.isEmpty()){
            Customer customer = new Customer(firstName, lastName);
            entityManager.persist(customer);
        }else{
            customer = customers.get(0);
        }
    }
    return customer;
}
seanizer
I need to make sure that the object, in your case, Customer doesn't already exist. I tried adding synchronization but the performance was far to poor.
TofuBeer
+1  A: 

Short sad answer is no the JPA API cannot do that for you. The whole API is more or less built around the optimistic principle of assuming things are going to work and throwing exceptions in the event of concurrent modification.

If this is happening often, there's likely some other component (whatever generates foo?) that could benefit from being made threadsafe, as perhaps an alternative to synchronizing around the query+create.

Affe
I have to do it in the reverse, what creates Foo is out of my control, and it is pretty much guaranteed to have duplicates. I'll have to gather up all the data form the threads and then store store them all on a single thread.
TofuBeer
+1  A: 

Some "hack" to consider:

  • implement hashCode() and equals() based on the business key of the objects (not the generated id)
  • synchronize on:

    (obj.getClass().getName() + String.valueOf(hashCode())).intern()
    

Thus you will get locks only in the relevant cases.

Bozho
Hmmm... I already have such equals/hash code... Never thought about synchronizing on them. I'll give it a shot just to see.
TofuBeer
that's clever :-)
seanizer