Question

I seem to have missed something in JPA's criteria API and its type safety. Consider the following code:

@Entity
@Access(FIELD)
class User(

  @Id
  Long id;

  @Column(unique=true)
  String email;

  String password;
}

Here the meta model:

@StaticMetamodel(User.class)
public static class User_ {
  public static volatile SingularAttribute<User, Long> id;
  public static volatile SingularAttribute<User, String> email;
  public static volatile SingularAttribute<User, String> password;
}

Then some code to exercise the class, built using pages from the Java EE Tutorial:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.select(user);
cq.where(cb.equal(user.get(User_.email), "john@google.com")); //this line is my problem

TypedQuery<User> q = em.createQuery(cq);
List<User> allUsers = q.getResultList();

assertEquals(1, allUsers.size());

It works fine. However, if I change the "where" clause to use an Integer instead of a String ("john@google.com"), I expected the code to not compile. Yet it compiles fine.

I thought the criteria API was supposed to be type safe? That is hardly more type safe than the following, using standard JPQL. I mean, what is the purpose of the meta model in the above code?? I have gained nothing from it.

User u = em.createQuery("select u from User u where u.email = :email", User.class)
           .setParameter("email", "john@google.com")
       .getSingleResult();

So the question is: can I make the criteria API query more type safe, so that I can only pass a String to the "from" clause?

Was it helpful?

Solution

Type safety is restricted to the upper bound of the generic type of the Expression<T> interface, not the exact type defined in the metamodel. So, because CriteriaBuilder.equal(Expression<?> x, java.lang.Object y) takes an argument of type Expression<?> it allows to pass any object for comparison.

The other CriteriaBuiler methods are more type safe, for example CriteriaBuilder.ge(Expression<? extends java.lang.Number> x, Expression<? extends java.lang.Number> y) allows only numbers. But allows to compare an integer field with a float for example.

You can't do better than this. The methods should have been something like CriteriaBuilder.equal(Expression<T> x, T y) with T being the field type from the metamodel.

Of course this is a hole in the type safety of the criteria API. I don't know why the JPA API creators choosed the wildcard version of these methods.

OTHER TIPS

If you really want strict type-matching you can use in() and your query would look like the following:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.select(user);
cq.where(cb.in(user.get(User_.email)).value("john@google.com"));

TypedQuery<User> q = em.createQuery(cq);
List<User> allUsers = q.getResultList();

assertEquals(1, allUsers.size());

or you could even write

cq.where(cb.in(user.get(User_.email)).value(cq.literal("john@google.com")));

While there are differences between using equal() and an in()-clause with only one value this achieves strict typing according to your static type model. Most database engines will optimize = and in() with one value in the same way.

If you don't like to switch equal() for in() you can also write functions like this to ensure your compiler alerts you of possible mistypings

static <T> Predicate equal(CriteriaBuilder cb, Expression<T> left, T right) {
    return cb.equal(left, right);
}

static <T> Predicate equal(CriteriaBuilder cb, Expression<T> left, Expression<T> right) {
    return cb.equal(left, right);
}

Lastly wether you like to use your own equal()-wrapper or switch to in() you may need the toLong(),... functions of CriteriaBuilder more often as the Number-objects aren't very cast-compatible and it is normally better to let the database handle integer-width changes.

cq.where(booelan , String) Java commits "implicit type casting of numbers to strings". The integer is directly fed to initialize a methods String type argument , so therefore it is a legitimate string. Around the only place and way in the language that problem could ever occur. You will simply be required to check the address syntax for validity first and consider how to handle such a problem if it occurs.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top