JPA Error-Handling for the Play Framework Asynchronous Setup

play jpa error

This is valid for the Play Framework 2.8 Release

Introduction
Recently, Play Framework added asynchronous processing for blocking operations. JPA operations are generally I/O intensive and is it is one of those blocking operations that Play is recommending to process asynchronously.

Play does so, by using Java “CompletionStage” interface and the “CompletableFuture” Future implementation which basically a task invoked in a separate thread that is not blocking the calling controller method.

For more information, check the Play documentation here.

This is a quick example on how it’s done in Play

Create a domain object and annotate the object to attach to a table and automatically generate a unique sequential id.

@Entity
@Table(name = "accounts")
@GeneratedValue(strategy = GenerationType.IDENTITY)
public class User  {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name="";
    private String pwd="";
    private String email="";
    public User() {

    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPwd() {
        return pwd;
    }
    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long userId) {
        this.id = userId;
    }
}

You need to create the JPA Repository interface

@ImplementedBy(JPAUserRepo.class)
public interface UserRepo {
    public CompletionStage<User> getUser(String userId);
    public  CompletionStage<User> save(User user);  
    public  CompletionStage<User> update(User user);
    }

And, finally create the JPA Repository Implementation Class

public class JPAUserRepo implements UserRepo{

    private final JPAApi jpaApi;
    private final DatabaseExecutionContext executionContext;
    @Inject
    public JPAUserRepo(JPAApi jpaApi, DatabaseExecutionContext 
    executionContext) {
        this.jpaApi = jpaApi;
        this.executionContext = executionContext;
    }
    private <T> T invokeJPA(Function<EntityManager, T> function) {
        return jpaApi.withTransaction(function);
    }   
    /**
    * Wrapping JPA functions in a generic invoke function that would 
    * be called by the Asynchronous CompletionStage Future 
    * implementation, this is just to shorten the boilerplate code
    **/
    @Override
    public CompletionStage<User> getUser(String userId){
        return supplyAsync(()->invokeJPA(em -> getUserByUserIdJPA(em, 
        userId)),executionContext);
    }

    public User getUserJPA(EntityManager em, String userId){
        User user = null;
        user= em.createQuery("SELECT u FROM User u WHERE u.id = 
        :id", User.class)
                .setParameter("id", userId)
                .getSingleResult();
        return user;
      /*
       * A simpler option return em.find(User.class,id);
       * This is for illustrating a more complex query
       */
    }
  
    @Override
    public CompletionStage<User> save(User user) {
        return supplyAsync(()->invokeJPA(em -> saveJPA(em, 
        user)),executionContext);
    }
    
    public User saveJPA(EntityManager em, User user){
        em.persist(user) ;
        return user;
    }
    @Override
    public CompletionStage<User> update(User user) {
        return supplyAsync(()->invokeJPA(em -> updateJPA(em, 
        user)),executionContext);
    }
    public User updateJPA(EntityManager em, User user){
        em.merge(user) ;
        return user;
    }
  }

JPA Error Handling
As you noticed the JPA methods will not force the exception handling, assuming the SQL exceptions will be caught by the caller. However, this is one area where you might experience a higher probability of Run Time exceptions, not just SQL Exception, for example if you change the ID type to String,

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)

private String id;

A Run-time exception will occur and it will not be visible almost anywhere,

m.i.k.s.a.JPAAccessAuditRepo - javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist

In this situation the Controller is not aware of the failure or the exception, and it might relay the wrong message back to the caller and it will not alert the system manager. How to remedy this?

First of all, it’s a good idea to wrap the EntityManager JPA call in a try/catch block to catch and report the exception properly.

Second; luckly, the JPA call should return a Future, and so, you can assign a callback method to handle the error using thenAccept.

This is the link to the full Play Example

Play is a Reactive Architecture based framework and employees Akka, the performant Actor model implementation that supports both Java and Scala. Play is supported by Lightbend.

Logic Keepers is a Lightbend Consulting partner. Logic Keepers provide solutions based on the Reactive Architecture methodology and the Lightbend Cloud-Native innovative technologies such as Play and Akka.