1
votes

Given a simple Objectify v5 entity like

@Entity
@Cache
public class Thing {
    @Id
    public String uid;
    public int a;
    public int b;
}

What is the correct way of updating existing entities when I want the following to be true

  • 0 database operations when the update changes nothing. I.e. the cache should be used and not invalidated when there is no property change.
  • consistency: independent, concurrent updates of properties a and b must not revert changes of the other property back => transaction

Without detection of changes, the following should work

public static Thing updateA( final String uid, final int value ) {

    return ofy().transact(new Work<Thing>() {
        @Override
        public Thing run() {
            Thing thing = ofy().load().type(Thing.class).id(uid).safe();
            thing.a = value;
            ofy().save().entity(thing);
            return thing;
        }
    });
}

However, since Objectify - to my knowledge - does not detect changes, above code would actually overwrite the entity, flush the cache, etc. Exactly what I try to prevent.

If saving is made conditional like

if (thing.a != value) {
    thing.a = value;
    ofy().save().entity(thing);
} else {
    // consistency guarantees w/o save?
    // need to transaction rollback / commit?
}

would that work as desired? In other words, what happens with a transaction that just loads a value but never saves one? My understanding of optimistic locking is that some action at the end of an transaction needs to verify that the state is as desired. That feels like it's missing when not saving.

1

1 Answers

1
votes

You've said a couple conflicting things here, but let me see if I can sort this out for you:

  • You cannot update entities safely with 0 database operations. The cache always represents stale data. The only safe way to update data is by performing a database read (and, if appropriate, a write) in a transaction. A read will cost you 1 operation.

  • Transactions sort themselves out by rolling back and retrying conflicts. They have the effective behavior of being applied serially. If you perform idempotent updates in a transaction, you will never have the "writes stepping on each other" problem. This is a combination of datastore behavior (error on conflict) and Objectify behavior (retrying on conflict).

You may wonder if you can check the cache to see if you can avoid starting a real transaction. Not if you care about losing updates. By the time you read and check a value, it may have already changed in the database. Your update is lost because it has a poorly-defined ordering. Transactions give you predictable (serial) order of updates.

Of course there's another way of solving this problem, which is to somehow guarantee that your entity never sees contention. That's often a very hard problem, which is why we have transactions.