November 30, 2013

Replace Play @Transactional with something better

10  comments

One of the jpa issues I found in play 2 is the behaviour of the transaction handling. The default play @Transactional does some kind of magic commit. Lets have a look at the following controller action.
@Transactional
public static Result magicCommit(String name){
    Task task = Task.findByName(name);
    task.name = "replacePlayTransactionalWithSomethingBetter";
    return ok();
}
The problem is the variable task.name is changed in the hibernate persistence context and the change is saved on transaction commit. Of course you can use @Transactional(readOnly = true). But it's easy to get this wrong. Of course using readonly = true is not possible on actions they might only update a model under some conditions. In this case you might just confuse a = with == and you could have a nasty data changing bug in your application. A much better approach would be: Only commit a transaction when an explicit save, update or delete is called from your model. The best way to realize this is a needsCommit thread local variable. The variable will be set to false by the transaction wrapper and to true inside the model methods. The annotation an the call would look like the original play solutions:
@With(TxAction.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tx {
    String value() default "default";
    boolean readOnly() default false;
}
public class TxAction extends Action{

    @Override
    public Result call(final Context ctx) throws Throwable {
        return Db.withTx(
                configuration.value(),
                configuration.readOnly(),
                new play.libs.F.Function0() {
                    @Override
                    public Result apply() throws Throwable {
                        return delegate.call(ctx);
                    }
                }
        );
    }
}
The implementation of the withTx method looks like this:
public class Db extends play.db.jpa.JPA {

    private static final play.Logger.ALogger log = play.Logger.of(Db.class);

    private static final ThreadLocal needsCommit = new ThreadLocal();

    /**
     * Run a block of code in a JPA transaction.
     *
     * @param name The persistence unit name
     * @param readOnly Is the transaction read-only?
     * @param block Block of code to execute.
     */
    public static  T withTx(String name, boolean readOnly, play.libs.F.Function0 block) throws Throwable {
        EntityManager em = null;
        EntityTransaction tx = null;
        try {
            em = play.db.jpa.JPA.em(name);
            play.db.jpa.JPA.bindForCurrentThread(em);
            bindNeedsCommitToThread();

            if (!readOnly) {
                tx = em.getTransaction();
                tx.begin();
            }

            T result = block.apply();

            if (tx != null && tx.isActive()) {
                if (needsCommit() && !tx.getRollbackOnly()) {
                    tx.commit();
                }
                else {
                    tx.rollback();
                }
            }
            return result;
        }
        catch (Throwable t) {
            if (tx != null && tx.isActive()) {
                try {
                    tx.rollback();
                }
                catch (Throwable e) {
                    // Ignore errors on rollback
                    log.error("Error on rollback!", e);
                }
            }
            throw t;
        }
        finally {
            bindNeedsCommitToThread();
            play.db.jpa.JPA.bindForCurrentThread(null);
            if (em != null) {
                em.close();
            }
        }
    }

    /**
     * Bind an EntityManager to the current thread.
     */
    public static void bindNeedsCommitToThread() {
        needsCommit.set(Boolean.FALSE);
    }

    /**
     * If set commit needed is set. The transaction is commited
     * after the block execution.
     */
    public static void setCommitNeeded() {
        needsCommit.set(Boolean.TRUE);
    }

    private static boolean needsCommit() {
        final Boolean b = needsCommit.get();
        return (b != null && b.booleanValue());
    }
}
In your model you need to set the commit variable to true like this:
public final void save() {
    JPA.em().persist(this);
    Db.setCommitNeeded();
}
Now you have a much more solid solution for your transactional handling. That only commits when you call a save in your model. You find the code in the play4jpa project on github.

Tags

commit, Hibernate, Java, JPA, Play Framework, Play4JPA, transaction


You may also like

Blog url changed to https

I just changed the url of this blog to https://jensjaeger.com. TLS encryption is now the default for all request to this page. It might be possible that some image links on some articles are hard coded http. If you find such an error it would be nice if you leave me comment so i can

Read More

Format date and time in java with prettytime

Prettytime is a nice java library to format a java Date()s in a nice humanized ago format, like: moments ago 2 minutes ago 13 hours ago 7 months ago 2 years ago Prettytime is localized in over 30 languages. It’s super simple to use Add the dependency to your maven pom: org.ocpsoft.prettytime prettytime 3.2.7.Final or

Read More