Some APIs have left a bad taste in us.
We changed the order of some calls, and suddenly nothing worked any more.
We forgot to use the return of some method, and it unexpectedly threw an exception.
The JPA Criteria API
is relatively mature concerning such problems.
Nevertheless I would like to capture the answers to some questions
that left me with some uncertainty when I started to work with that API.
Questions
Do I have to call select(), where(), orderBy(), ...
in the same order as an SQL query would require,
or can I call them in any order?
All these calls return a CriteriaQuery object,
do I have to use that return for further actions,
or can I use the initial instance for all subsequent calls?
When I call select(), where(), orderBy(), ... a second time,
would they overwrite any preceding call, or add to them?
Due to unknown communication problems,
developers are used to trying out everything by themselves.
So did I, here comes my test.
Answers
You find the entities used in this example in my recent article about
JOIN-types.
Following is a query that joins two tables and uses
select(), where(), orderBy() in an unusual order,
and uses the return from last call as final query:
1
2
3
4
5
6
7
8
9
10
11
12
13
finalCriteriaBuildercb=entityManager.getCriteriaBuilder();finalCriteriaQuery<Object[]>query=cb.createQuery(Object[].class);finalRoot<City>city=query.from(City.class);finalJoin<City,House>house=city.join("houses");finalCriteriaQuery<Object[]>completeQuery=query.orderBy(cb.asc(city.get("name"))).where(cb.like(cb.upper(city.get("name")),"%A%")).multiselect(city.get("name"),house.get("name"));// groupBy(), having(), select(), ... all return the same query,// so there is no need for capturing the last returned completeQuery!returnentityManager.createQuery(completeQuery).getResultList();
It is not necessary to use the query returned by the calls to CriteriaQuery,
like I do here with completeQuery.
All methods return the very query instance they were called on, this programming style is called
fluent interface.
Also it is not necessary to keep the calls in a specific order.
Following query delivers exactly the same result as the first one,
although implemented in a different way:
1
2
3
4
5
6
7
8
9
10
11
12
finalCriteriaBuildercb=entityManager.getCriteriaBuilder();finalCriteriaQuery<Object[]>query=cb.createQuery(Object[].class);finalRoot<City>city=query.from(City.class);finalJoin<City,House>house=city.join("houses");query.multiselect(house.get("name"),city.get("name"));query.multiselect(city.get("name"),house.get("name"));// this overwrites the preceding multiselect() callquery.where(cb.like(cb.upper(city.get("name")),"%A%"));query.orderBy(cb.asc(city.get("name")));returnentityManager.createQuery(query).getResultList();
In this second example, on line 6, I call multiselect()
for selecting several fields instead of an object.
On line 7, I call the same method again, but with different parameters.
This demonstrates a problem with fluent interface:
do subsequent calls replace any preceding call, or do they merge into it, or would they cause an exception?
As you can see by the inline comment, they replace.
This is also documented in
JavaDoc.
Thus several calls to the same method of CriteriaQuery do not make sense,
because they overwrite each other.
Rules of thumb:
Get a CriteriaBuilder from EntityManager
Fetch a CriteriaQuery with a result-type from CriteriaBuilder
Call from() with a root-type on the CriteriaQuery
to get a query-root, used to name attributes in where() and others
Optionally the same with join()
Then call select() or multiselect(),
where(), orderBy(), groupBy(), having(),
either one-by-one or in fluent interface style, in any order,
but each just once
Finally wrap the CriteriaQuery into a TypedQuery
using the EntityManager, and actually read some result from database.
Conclusion
Key to understanding the API is that calls to CriteriaQuery
just build a query, they do not yet read anything from database.
Reading is done by the final entityManager.createQuery(criteriaQuery).getResultList() call.
OUTER is not a join-type, it is a category.
It means that more records than just the related ones will be in the query's result set.
All joins except INNER and SELF implicitly are outer joins: LEFT, RIGHT, FULL.
The words "left" and "right" refer to the side of the JOIN keyword
where the outer-joined table is.
In the following I will show just INNER, LEFT, RIGHT and FULL joins.
When I tried them out with the JPA Criteria API,
I discovered that just INNER and LEFT are supported!
Example
To illustrate the different join keywords, I use a minimal set of data, consisting of
cities and houses.
A city contains zero-to-many houses, a house can belong to a city.
Thus they do not depend on each other, but are in some way hierarchical.
Following are the different SQL joins and their result lists.
INNER
SELECTc.NAME, h.NAME
FROM
CITY cJOIN
HOUSE h onc.id = h.CITY_ID
CITY.NAME
HOUSE.NAME
Washington
Pentagon
We could also write "INNER JOIN". This is the default join.
There is just one result row, because there is just one city connected to a house.
LEFT
SELECTc.NAME, h.NAME
FROM
CITY cLEFTJOIN
HOUSE h onc.id = h.CITY_ID
CITY.NAME
HOUSE.NAME
Washington
Pentagon
Aleppo
(NULL)
We could also write "LEFT OUTER JOIN".
There are two result rows, all cities with or without a house.
The outer joined table is City, being left of the JOIN keyword.
RIGHT
SELECTc.NAME, h.NAME
FROM
CITY cRIGHTJOIN
HOUSE h onc.id = h.CITY_ID
CITY.NAME
HOUSE.NAME
Washington
Pentagon
(NULL)
Kremlin
We could also write "RIGHT OUTER JOIN".
There are two result rows, all houses with or without a city.
The outer joined table is House, being right of the JOIN keyword.
This also could be written as LEFT JOIN when swapping the tables in FROM and JOIN clauses,
thus the RIGHT JOIN is rarely used in real world.
SELECTc.NAME, h.NAME
FROM
HOUSE h
LEFTJOIN
CITY conc.id = h.CITY_ID
FULL
SELECTc.NAME, h.NAME
FROM
CITY cFULLJOIN
HOUSE h onc.id = h.CITY_ID
CITY.NAME
HOUSE.NAME
Washington
Pentagon
Aleppo
(NULL)
(NULL)
Kremlin
We could also write "FULL OUTER JOIN".
This combines LEFT and RIGHT joins, thus there are three result rows,
all cities with or without a house, and
all houses with or without a city.
Mind that this is not a
cartesian product of both tables!
Java Source Code
Entities
Here come the Java persistence classes used by following unit-test.
City.java
importjava.util.*;importjavax.persistence.*;importfri.jpa.util.BacklinkSettingSet;@EntitypublicclassCity{@Id@GeneratedValueprivateLongid;@Column(unique=true)privateStringname;/** The houses of this city. */@OneToMany(mappedBy="city",cascade=CascadeType.ALL,orphanRemoval=true)privateSet<House>houses=newHashSet<>();publicObjectgetId(){returnid;}publicSet<House>getHouses(){returnnewBacklinkSettingSet<House,City>(houses,this,(element,owner)->element.setCity(owner));}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}}
House.java
importjavax.persistence.*;@EntitypublicclassHouse{@Id@GeneratedValueprivateLongid;@Column(unique=true)privateStringname;/** The city this house is located at. */@ManyToOneprivateCitycity;publicObjectgetId(){returnid;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicCitygetCity(){returncity;}voidsetCity(Citycity){this.city=city;}}
The unit-test builds the test data described above,
and carries out joins using the Criteria-API.
Please see my recent article about setting up a JPA project
for how you can make the abstract test run
with both Hibernate and EclipseLink.
JpaJoinTest is the skeleton class, for now without any test method.
It implements set-up (test-data build) and tear-down (cleanup),
and prepares utilities like the JPA EntityManager
for the tests to come.
Please put all methods listed below into it.
JpaJoinTest.java
importstaticorg.junit.Assert.*;importjava.util.List;importjavax.persistence.*;importjavax.persistence.criteria.*;importorg.junit.*;publicabstractclassJpaJoinTest{// setupprivateEntityManagerentityManager;/** @return the name of the persistence-unit to use for all tests. */protectedabstractStringgetPersistenceUnitName();privateHousekremlin;privateHousepentagon;privateCitywashington;privateCityaleppo;/** * Builds test data: one city with a house, * one city without house, and one house without city. */@BeforepublicvoidsetUp(){entityManager=Persistence.createEntityManagerFactory(getPersistenceUnitName()).createEntityManager();// build test data// house without citykremlin=newHouse("Kremlin");// house with citypentagon=newHouse("Pentagon");// city with housewashington=newCity("Washington");washington.getHouses().add(pentagon);// city without housealeppo=newCity("Aleppo");JpaUtil.persistAll(newObject[]{kremlin,aleppo,washington},entityManager);// washington will cascade-persist pentagon}@AfterpublicvoidtearDown(){JpaUtil.removeAll(newClass<?>[]{City.class,House.class},entityManager);}/** Join query result class. */publicstaticclassCityAndHouse{publicfinalCitycity;publicfinalHousehouse;publicCityAndHouse(Citycity,Househouse){this.city=city;this.house=house;}}privateHousenewHouse(Stringname){finalHousehouse=newHouse();house.setName(name);returnhouse;}privateCitynewCity(Stringname){finalCitycity=newCity();city.setName(name);returncity;}}
JpaUtil provides saving, listing and deleting entities
in context of transactions. It uses Java 8 functional lambda features.
JpaUtil.java
importjava.util.List;importjava.util.function.Consumer;importjavax.persistence.EntityManager;importjavax.persistence.EntityTransaction;publicfinalclassJpaUtil{/** * @param <T> the class of the objects in returned result list. * @param persistenceClass the database table to be queried. * @param em JPA database access. * @return all records from given table. */publicstatic<T>List<T>findAll(Class<T>persistenceClass,EntityManagerem){returnem.createQuery("select x from "+persistenceClass.getName()+" x",persistenceClass).getResultList();}/** * @param persistenceClass the database table to be queried. * @param em JPA database access. * @return the number of records in given table. */publicstaticLongcountAll(Class<?>persistenceClass,EntityManagerem){returnem.createQuery("select count(x) from "+persistenceClass.getName()+" x",Long.class).getSingleResult();}publicstaticvoidpersistAll(Object[]entities,EntityManagerem){JpaUtil.transactional(em,(toPersist)->{for(Objectentity:toPersist)em.persist(entity);},entities);}publicstaticvoidremoveAll(Class<?>[]persistenceClasses,EntityManagerem){JpaUtil.transactional(em,(entityTypes)->{for(Class<?>entityType:entityTypes)for(Objectentity:findAll(entityType,em))em.remove(entity);},persistenceClasses);}/** @return the given parameter. */publicstatic<P>Ptransactional(EntityManagerentityManager,Consumer<P>entityManagerFunction,Pparameter){finalEntityTransactiontransaction=entityManager.getTransaction();try{transaction.begin();entityManagerFunction.accept(parameter);transaction.commit();returnparameter;}catch(Throwableth){if(transaction.isActive())transaction.rollback();throwth;}}privateJpaUtil(){}// do not instantiate}
Criteria Join
Following is a join-implementation using the JPA Criteria API:
1
2
3
4
5
6
7
8
9
10
11
12
privateList<CityAndHouse>runCityJoin(JoinTypejoinType){// build queryfinalCriteriaBuildercb=entityManager.getCriteriaBuilder();finalCriteriaQuery<CityAndHouse>query=cb.createQuery(CityAndHouse.class);finalRoot<City>city=query.from(City.class);finalJoin<City,House>house=city.join("houses",joinType);query.select(cb.construct(CityAndHouse.class,city,house));query.orderBy(cb.asc(city.get("name")),cb.asc(house.get("name")));// run queryreturnentityManager.createQuery(query).getResultList();}
The parameter joinType determines how the JOIN will work.
The query starts with City and joins House.
It uses CityAndHouse (inner class prepared by JpaJoinTest) as result class.
There is also an order-by clause to sort the result list,
needed by test-assertions.
If you have meta-data for your entity-types by hand (such can be generated by the JPA-provider),
please use these instead of my string properties "houses" and "name".
As JPA does not support RIGHT JOIN, I also implemented the substituting LEFT JOIN that gives the same result.
The implementation is the same as above, just City and House have been swapped:
1
2
3
4
5
6
7
8
9
10
11
12
privateList<CityAndHouse>runHouseJoin(JoinTypejoinType){// build queryfinalCriteriaBuildercb=entityManager.getCriteriaBuilder();finalCriteriaQuery<CityAndHouse>query=cb.createQuery(CityAndHouse.class);finalRoot<House>house=query.from(House.class);finalJoin<House,City>city=house.join("city",joinType);query.select(cb.construct(CityAndHouse.class,city,house));query.orderBy(cb.asc(city.get("name")),cb.asc(house.get("name")));// run queryreturnentityManager.createQuery(query).getResultList();}
Test Methods
Now finally here are the tests, with beautiful descriptive names:
/** * An inner join contains just connected left and right entities. */@TestpublicvoidinnerJoinShouldFindCitieswWithHousesOnly(){finalList<CityAndHouse>result=runCityJoin(JoinType.INNER);// INNER is defaultassertEquals(1,result.size());finalCityAndHousecityAndHouse=result.get(0);assertEquals(washington,cityAndHouse.city);assertEquals(pentagon,cityAndHouse.house);}/** * A left join is an outer join. * It contains connected and unconnected left entities, * but only connected right entities. */@TestpublicvoidleftJoinShouldFindCitieswWithAndWithoutHouses(){finalList<CityAndHouse>result=runCityJoin(JoinType.LEFT);assertEquals(2,result.size());finalCityAndHousealeppoAndNull=result.get(0);assertEquals(aleppo,aleppoAndNull.city);assertNull(aleppoAndNull.house);finalCityAndHousewashingtonAndPentagon=result.get(1);assertEquals(washington,washingtonAndPentagon.city);assertEquals(pentagon,washingtonAndPentagon.house);}/** * A right join is an outer join. * It contains only connected left entities, * but connected and unconnected right entities. */@TestpublicvoidreverseLeftJoinShouldFindCitiesWithHousesAndHousesWithoutCity(){finalList<CityAndHouse>result=runHouseJoin(JoinType.LEFT);assertRightJoin(result);}/** * JPA: "Right outer joins and right outer fetch joins * are not required to be supported in Java Persistence 2.0. * Applications that use RIGHT join types will not be portable." * @see https://docs.oracle.com/javaee/6/api/javax/persistence/criteria/JoinType.html */@Test@Ignore("Both Hibernate/EclipseLink throw UnsupportedOperationException: RIGHT_JOIN_NOT_SUPPORTED")publicvoidrightJoinShouldFindCitiesWithHousesAndHousesWithoutCity(){finalList<CityAndHouse>result=runCityJoin(JoinType.RIGHT);assertRightJoin(result);}privatevoidassertRightJoin(List<CityAndHouse>result){assertEquals(2,result.size());finalCityAndHousewashingtonAndPentagon=result.get(0);assertEquals(washington,washingtonAndPentagon.city);assertEquals(pentagon,washingtonAndPentagon.house);finalCityAndHousenullAndKremlin=result.get(1);assertNull(nullAndKremlin.city);assertEquals(kremlin,nullAndKremlin.house);}/** * A full join is an outer join. * It contains connected and unconnected left entities * and connected and unconnected right entities. * <p/> * But: JPA JoinType does not provide the full outer join type! */@Test@IgnorepublicvoidfullOuterJoinShouldFindCitiesWithOrWithoutHousesAndHousesWithoutCity(){finalList<CityAndHouse>result=runCityJoin(JoinType.valueOf("FULL"));// there is no JoinType.FULL !assertEquals(3,result.size());finalCityAndHousealeppoAndNull=result.get(0);assertEquals(aleppo,aleppoAndNull.city);assertNull(aleppoAndNull.house);finalCityAndHousenullAndKremlin=result.get(1);assertNull(nullAndKremlin.city);assertEquals(kremlin,nullAndKremlin.house);finalCityAndHousewashingtonAndPentagon=result.get(2);assertEquals(washington,washingtonAndPentagon.city);assertEquals(pentagon,washingtonAndPentagon.house);}
The INNER and the LEFT test are the only ones that worked.
I had to retrofit the RIGHT test by swapping the entity-types to emulate the RIGHT JOIN
(I called it reverseLeftJoinShouldFindCitiesWithHousesAndHousesWithoutCity).
The real RIGHT test and the FULL test have been @Ignored.
I left them inside just for completeness.
The assertions make sure that the results are like what I listed in the introduction.
The order-by clause of the query made the get-indexes of the results reliable.
Postgres Database
To make sure that my results are realistic I used, besides
H2, another database.
Postgres
is an old but powerful UNIX database that is freely available.
It is pre-installed on most LINUX distributions.
To find out if it is on your machine, open a command line terminal and type "psql --help".
If it answers, you can type "psql" and then list your databases by typing "\l".
You will see that template1 is present, which you could use for tests.
You can then connect via "\c template1" to that database,
where "\d" will describe tables, views and sequences.
With "\q" you can quit.
Make sure in your postgresql.conf that listen_addresses = 'localhost'
(which is default), so that it rejects connections other than your own machine.
Here are my JPA persistence.xml connection properties for PostgreSQL:
You may have noticed that the entities City and House do not
extend BaseEntity (that provides an always-present application-made UUID).
This is because Postgres has a bug with primary keys of type UUID:
PSQLException: ERROR: column "id" is of type uuid but expression is of type bytea
Workaround would be to have a 36-character String id that represents the UUID.
Conclusion
When I started to work with databases in the Nineties, there were just INNER and OUTER joins.
Looks like we are still there when using JPA.
But - who needs more?
Do these additional keywords, made for maybe 10% of the users, really make sense?
Didn't they just add complexity that made life for the other 90% much harder?