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.
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.
There is already a basic implementation of a play jpa finder:
https://gist.github.com/jensjaeger/93da1c8c0bf8f627fae3
I will integrate a more advanced version of the finder in the play4jpa project soon. Hopefully I also find time to blog about it as well.
Happy coding
Jens
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.
This implementation is only a snippet and not in your project yet ?
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
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.
Hey JJ,
there is no more JPA related magic in play 2, like you know from play 1.
Jens
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 ?
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.
Hey JJ,
a extended version of the finder was pushed to https://github.com/jensjaeger/play4jpa. A blog post, unit test and more documentation is in progress.
Jens