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< Tx>{

    @Override
    public Result call(final Context ctx) throws Throwable {
        return Db.withTx(
                configuration.value(),
                configuration.readOnly(),
                new play.libs.F.Function0< Result>() {
                    @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< boolean> 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> T withTx(String name, boolean readOnly, play.libs.F.Function0< T> 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

Leave a Reply

Your email address will not be published. Required fields are marked

Information about Data protection

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. Nice thanks for the code.

    It’s sad because all the integration with ebean was already there.

    We will have to write our own finder (or copy the one in play 1.0)

    I’ll check your code on github to see if I integrate it in my project.

  2. Yeah, really good work.

    I just don’t know if I should go with EBean, JPA or your version of JPA. Because with the next update of play, it’s sure that my code will be incompatible with the new api(if I use any) unless they use your project as a base.

    But Ebean will still be supported as a plugin.

    It’s difficult to choose because it will be used in a really critical component of our system.

  3. Hey JJ,

    thanks for your comments.

    If you use Hibernate you will be compatible with any 2.x Version of Play. In 2.3 JPA will be the default in favor of ebean. If you use the @Tx Annotation instead of the default play @Transactional you still compatible, but don’t use the withTransaction Part of Play Framework you use a custom one with another (i think better) behavior.

    After some research I decided that for me hibernate is a better foundation for a new project than ebean. Other think the same. There is a reason that eBean will be replaced with JPA in Play 2.3.

    The @Tx implementation in this post is extracted from a big client project im working on at the moment. In the future I will write some more about our hibernate jpa implementations here.

    Jens

  4. Yess, I know it’ll be compatible with the JPA annotation. It’s just, it won’t come with all the play magic, or I’ll have to recode some part of the app.

  5. I decided to use your gist for JPA finder and it work great.

    The only thing I couldn’t get to work is with @ManyToOne

    If I have en entity with 2 variables

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    public Long id;
    @ManyToOne(fetch=FetchType.EAGER)
    public OtherModel ortherModel;

    .eq(“otherModel.code”, “value”)

    It shows a could not resolve property otherModel.code
    Maybe there’s something I don’t know in jpa. Because in JPQL it would work but I don’t know Criteria api.

    Do you have an idea ?

  6. I was able to make it work, but I needed to add a method join() to your model.

    See it there : https://gist.github.com/miclefebvre/16b26137d886652e65d8

    It’s not as nice as in Ebean but it does the job
    If we have 4 tables it will look like

    find.where()
    .join(“tableA”, “tableA”)
    .join(“tableA.tableB”, “tableB”)
    .join(“tableB.tableC”, “tableC”)
    .eq(“tableC.value”, value)

    It would have been nice and easy to have the Finder create the join automatically if it detect if the field in eq (for example) as multiple dot (.) tableA.tableB.tableC but it wont work if we add other restriction like:

    .or(Restrictions.isNull(“tableA.Table.date”),
    Restrictions.lt(“tableA.Table.date”, now))

    Let me know if you have other idea.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}