Skip to content

A type-safe named query design approach for JPA

The starting point

In a JEE you normally use the Java-Persistence-API JPA to access the data. JPA provides named queries which are statically defined queries with an unmodifiable query string. Because the query string is unmodifiable a client must still be able to provide parameters that concretize a query. Those query parameters are placed in the query string in a special format, e.g. :username.

So if we define a named query in the simplest way it will look like this:

@Entity
@NamedQueries(
  {
  @NamedQuery(
        name = "findOrdersByUsername",
        query = "select distinct o from Order o JOIN order.user AS u where u.username = :username")
  }
)
public class Order {
 ...
}

Once a named query is defined we can use it. The EntityManager provides access to it.

EntityManager em = ....;
TypedQuery<T> query = em.createNamedQuery("findOrdersByUsername", Order.class);
query.setParameter("username", "jack123");
List<Order> orders = query.getResultList();

This looks like a quite good solution. Just the client code must know the query parameters that we defined in named query. But this also means that we couple every client code to the query string and one developer who changes the query string might not know all places of client code where the query string parameters are used.

So, is there

  • better way?
  • a way that allows us to bring those things together that belong together?
  • a way to get compiler support?

In the following sections I will show you one approach of how to achieve these objectives.

Encapsulation of the named query

Before we start to improve something we should think about the problem from a client perspective.
The question always is “What would be a comfortable way for a client to use an API?”

If I look at the named queries in JPA from a client’s perspective, as a client I would like to write something like this:

OrdersByUsername ordersByUsername = new OrdersByUsername();
ordersByUsername.setUsername("jack123");

List<Order> orders = dao.find(ordersByUsername);

From a client’s perspective this looks like a comfortable and safe API.
Now lets start to implement exactly this.

First of all the client wants to create an object that represents the query called OrdersByUsername. Thus we create this class.

public class OrdersByUsername {

}

Because the OrdersByUsername represents a specific query we also define the query’s name, the query string and it’s parameter names in this class.

public class OrdersByUsername {

   public static final String NAME = "findOrdersByUsername";
   public static final String QUERY = "select distinct o from Order o JOIN order.user AS u where u.username = :username";
   private static final String PARAM_USERNAME = "username";

   private String username;

   public void setUsername(String username) {
       this.username = username;
   }
}

After a client created an OrdersByUsername object and set the username he wants to call a DAO to find the orders.

Thus we implement that feature in an AbstractDao. Thus we want to make this feature available for all DAOs we also have to find an abstraction for the OrdersByUsername class. Let’s name this abstraction NamedEntityQuery. Furthermore the AbstractDao must have access to the query’s name, the parameters and the query string. We also place this access in the NamedEntityQuery.

public abstract class AbstractDao<T, ID> {

  public List<T> find(NamedEntityQuery<T> namedEntityQuery) {
        Map<String, ?> params = namedEntityQuery.getParams();
        String queryName = namedEntityQuery.getName();
      return findByNamedQuery(queryName, params);
  }

  private List<T> findByNamedQuery(String queryName, Map<String, ?> params) {
        TypedQuery<T> query = getNamedQuery(queryName);
        applyQueryParams(query, params);
      return query.getResultList();
  }

  private TypedQuery<T> getNamedQuery(String queryName) {
        EntityManager em = getEntityManager();
        TypedQuery<T> query = em.createNamedQuery(queryName, getEntityClass());
      return query;  
  }

  private void applyQueryParams(TypedQuery<T> query, Map<String, ?> params){
      for (Map.Entry<String, ?> param : params.entrySet()) {
            query.setParameter(param.getKey(), param.getValue());
      }
  }

  ...

  public T getById(ID id){
      ...
  }
  ...

  protected abstract EntityManager getEntityManager();
}

Now we have to implement the abstraction of a named query that we use in the AbstractDao.

public abstract class NamedEntityQuery<T> {

   private final Map<String, Object> params = new HashMap<String, Object>();

   protected void setParam(String name, Object value) {
        params.put(name, value);
   }

   public Map<String, ?> getParams() {
       return params;
   }

   public abstract String getName();
}

And now we let OrdersByUsername inherit the NamedEntityQuery.

public class OrdersByUsername extends NamedEntityQuery<Order> {

  public static final String NAME = "findOrdersByUsername";
  public static final String QUERY = "select distinct o from Order o JOIN order.user AS u where u.username = :username";
  private static final String PARAM_USERNAME = "username";

  public void setUsername(String username) {
       setParam(PARAM_USERNAME, username);
  }

  public String getName(){
     return NAME;
  }
}

Finally we adapt the JPA named query definition.

@Entity
@NamedQueries(
   {
   @NamedQuery(
        name = OrderNamedQueries.OrdersByName.NAME,
        query = OrderNamedQueries.OrdersByName.QUERY)
   }
)
public class Order {
  ...
}

That’s it.

Closing Words

Someone might argue that the approach of decouple a query string and parameters from a client is a lot of stuff to do for just a simple query,
but someone should also keep in mind that this approach:

  1. Decouples the client from the query string.
    Thus the query string can be changed in order to refactoring the database without affecting the client.
  2. The query string parameter binding is type-safe and checked by the compiler.
    Even more sophisticated checks can be implemented like a not null check or a pattern match of the parameter to set.
    In the old approach this is also possible, but the validation logic will be scattered through the client code and might differ on any place.

I hope that this was an interessting blog about how things can else be done and that it inspired you so that you might develop a even better approach on your own.

If an API is not what we want it to be – we should change it.

Leave a Reply

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

 

GDPR Cookie Consent with Real Cookie Banner