Transactions are where Objectify departs most significantly from the patterns set by the Low-Level API and JDO/JPA. Objectify’s transactions are inspired by EJB and NDB transactions: Designed to allow modular, convenient transactional logic with a minimum of boilerplate.
You should familiarize yourself with the GAE Transactions documentation.
Basic Transactions
A basic transaction looks like this:
import static com.googlecode.objectify.ObjectifyService.ofy; import com.googlecode.objectify.Work; // If you don't need to return a value, you can use VoidWork Thing th = ofy().transact(new Work<Thing>() { public Thing run() { Thing thing = ofy().load().key(thingKey).get(); thing.modify(); ofy().save().entity(thing); return thing; } });
Idempotence
Work MUST be idempotent. A variety of conditions, including ConcurrentModificationException, can cause a transaction to retry. If you need to limit the number of times a transaction can be tried, use transactNew(int, Work).
Cross-Group Transactions
Objectify requires no special flags to enable cross-group transactions. If you access more than one entity group in a transaction, the transaction with be an XG transaction. If you do access only one, it is not. The standard limit of 5 EGs applies to all transactions.
Transaction Inheritance
Transaction contexts are inherited. In this example, the inner transact() does not start a new transaction:
public void doSomeWork() { ofy().transact(new VoidWork() { public void vrun() { doSomeMoreWork(); } } } public void doSomeMoreWork() { ofy().transact(new VoidWork() { public void vrun() { // When called from doSomeWork(), executes in the original transaction context // When called from outside a transaction, executes in a new transaction } } }
This makes it easy to create modular bits of transactional logic that can be used from a variety of contexts without having to pass around Objectify instances as parameters.
If you need to suspend a transaction and begin a new one, use the transactNew() method:
ofy().transactNew(new VoidWork() { public void vrun() { Thing thing = ofy().load().key(thingKey).get(); thing.modify(); ofy().save().entity(thing); } });
The old transaction (if present) is suspended for the duration of the new transaction. After the transaction commits (or rolls back), the original transaction will resume.
Escaping Transactions
There are often times in the middle of a transaction when you would like to make a datastore request outside of a transaction. Perhaps you wish to make a query without an ancestor(), or perhaps you wish to load an entity without enlisting it in a XG transaction.
Objectify makes it easy to run operations outside of a transaction:
ofy().transact(new VoidWork() { public void vrun() { Thing thing = ofy().load().key(thingKey).get(); Other other = ofy().transactionless().load().key(thing.otherKey).get(); if (other.specialState) { thing.modify(); ofy().save().entity(thing); } } });
This circumstance doesn’t come up often but it does come up.
Transactions and Caching
Starting a transaction creates a new Objectify instance with a fresh, empty session cache. Loads and saves will populate this new instance cache; because of this, entities modified will appear modified after being loaded again within the same transaction. Unlike the low-level API, the datastore will not appear “frozen in time”, although transaction isolation is maintained.
When a transaction successfully commits, its session cache will be copied into the parent Objectify instance’s session cache.
Transactions integrate correctly with Objectify’s global memcache. Reads and writes bypass the memcache, but a successful commit will reset the memcache value for changed entities.
execute()
Objectify provides a method designed to facilitate EJB-like behavior:
ofy().execute(TxnType.REQUIRED, new VoidWork() { public void vrun() { Thing thing = ofy().load().key(thingKey).get(); thing.modify(); ofy().save().entity(thing); } });
The TxnType enum provides the common EJB transaction attributes:
MANDATORY | If not already in a transaction, throw an exception. Otherwise, use the transaction. |
REQUIRED | If there is already a transaction in progress, use it. If not, start a new transaction. |
REQUIRES_NEW | If there is already a transaction in progress, suspend it. Always start a new transaction. |
SUPPORTS | If there is a transaction in progress, use it. Otherwise, execute without a transaction. |
NOT_SUPPORTED | If there is a transaction in progress, suspend it. Execute without a transaction. |
NEVER | If there is a transaction in progress, throw an exception. Otherwise, execute without a transaction. |
You will probably not use this method directly. However, you can easily use it to build AOP interceptors for Guice and Spring.
Transactions with Guice
Using Guice AOP you can place EJB-like annotations on methods and eliminate all the new Work<Blah>() {...} boilerplate:
public class Worker { @Transact(TxnType.REQUIRED) public void doSomeWork() { ofy().load()... // do some work Thing th = doSomeMoreWork(); ofy().save()... // do some work } @Transaction(TxnType.REQUIRED) public Thing doSomeMoreWork() { ofy().load()... // do some more work return someThing; } } Worker worker = injector.getInstance(Worker.class); worker.doSomeWork(); // all work done in a single transaction!
These methods are automatically wrapped in Work classes and executed with Objectify.execute().
This requires two classes:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Transact { TxnType value(); } public class TransactInterceptor implements MethodInterceptor { /** Work around java's annoying checked exceptions */ private static class ExceptionWrapper extends RuntimeException { private static final long serialVersionUID = 1L; public ExceptionWrapper(Throwable cause) { super(cause); } /** This makes the cost of using the ExceptionWrapper negligible */ @Override public synchronized Throwable fillInStackTrace() { return this; } } /** The only trick here is that we need to wrap & unwrap checked exceptions that go through the Work interface */ @Override public Object invoke(final MethodInvocation inv) throws Throwable { Transact attr = inv.getStaticPart().getAnnotation(Transact.class); TxnType type = attr.value(); try { return ofy().execute(type, new Work<Object>() { @Override public Object run() { try { return inv.proceed(); } catch (RuntimeException ex) { throw ex; } catch (Throwable th) { throw new ExceptionWrapper(th); } } }); } catch (ExceptionWrapper ex) { throw ex.getCause(); } } }
Now, to enable this interceptor, add this to your Guice module configuration:
public static class YourModule extends AbstractModule { @Override protected void configure() { bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transact.class), new TransactInterceptor()); // continue with your guice configuration } }
You can find a working example of this in the Motomapia sample application.
Transactions with Spring
TODO: How to integrate Spring’s transaction system, or link to a project that does.