I've read (only in snippets, and in passing reference in the GAE
documentation) that using an unencoded String key, aka "key name",
guarantees uniqueness of that user-provided field in the same way that
a Long field is unique (but generated by the backend).  However, it's
been really tough figuring out how to ensure that an exception is
thrown when there's an attempt to create a key name that already
exists.  There's been questions like this in the past with rather
vague answers and no explicit documentation of successful results.

My main confusion came from the fact that doing EntityManager.find
(...) to check for a previous instance would return null when there
was no existing entity with that key.  That seemed like a race
condition to me, because there was no explicitly documented "lock" on
that key name.  Further, I ran into some situations where simple
interleaving of requests would succeed, again supporting a possible
race condition.

I finally found the *exact* procedure which must be followed in order
to guarantee uniqueness on a key name.  Max et al., please check my
work and make sure this is right.  JPA notation is used here, but it
should translate to JDO appropriately (have not tried yet).

=====

1. Get and begin a new transaction (EntityManager.getTransaction() ->
EntityTransaction.begin())

2. Call EntityManager.find(ClassName.class, "keyname");

3. If find(...) did not return null, roll back the transaction
manually and stop here; optionally throw your own exception.
(Creating and persisting a new intance after this point WILL silently
overwrite the old instance.)

4. Create the new instance, setting the String @Id field to "keyname".

5. Call EntityManager.persist(...) on the new instance.  (JPA only:
Do not use the implicit persist-if-new logic of EntityManager.merge
(...).)

6. Call EntityTransaction.commit().  If another entity of the same
name was created during the course of this transaction, an exception
will be thrown at this point.  (JPA:  RollbackException, tracing to
original root ConcurrentModificationException in the low-level
Datastore API.)

=====

The concurrency control seems to kick in only when EntityManager.find
(...) returns null within a transaction -- and only when all such
accesses follow this pattern.  It's like the null return is an
implicit transaction-level lock on that key name.  That's why step 3
exists above; creating a new instance and persisting it anyway will
just succeed.

I did some extensive testing both in the dev server and production,
and it works as described above.  Below is a sample snippet that
demonstrates the concept.  Note that the object is not required to
have a @Version locking field.  (My apologies for abusing Object.wait
(...) to sleep on the production servers; it was the only way to
ensure that two competing requests both saw no pre-existing instance
and attempted a persist.)

=====
import javax.persistence.*;

@Entity
public class NameUniqueTest {
    @Id
    private String name;

    @Id
    public String getName() {
        return name;
    }

    @SuppressWarnings("unused")
    private void setName(String name) {
        this.name = name;
    }

    private String data;

    @Basic
    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public NameUniqueTest(String name) {
        this.name = name;
    }
}
=====
        Object waiter = new Object(); // this is part of testing;
don't do this :)

        String val = req.getQueryString();
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {
            synchronized (waiter) { // this is part of testing; don't
do this :)
                tx.begin();
                LOG.warn(val + ": starting txn");

                if (em.find(NameUniqueTest.class, "myname") != null)
                    throw new RuntimeException("instance already
exists");

                LOG.info(val + ": instance did not exist; creating one
and sleeping 10s");
                NameUniqueTest nu = new NameUniqueTest("myname");
                nu.setData(val);
                waitLock.wait(10000); // this is part of testing;
don't do this :)

                em.persist(nu);
                LOG.info(val + ": persisted before commit, sleeping
5s");
                waitLock.wait(5000); // this is part of testing; don't
do this :)

                tx.commit();
            }
        } catch (RuntimeException e) {
            LOG.error(val + ": caught exception", e);
        } finally {
            if (tx.isActive())
                tx.rollback();
            em.close();
        }
-- 
You received this message because you are subscribed to the Google Groups 
"Google App Engine for Java" group.
To post to this group, send email to google-appengine-j...@googlegroups.com.
To unsubscribe from this group, send email to 
google-appengine-java+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/google-appengine-java?hl=en.


Reply via email to