Blog-Archiv

Sonntag, 12. Januar 2020

JPA Criteria Query

The JPA Criteria API strives to provide all SQL capacities in an object-oriented way. It is the typed alternative to JPA query language, to be used with ORMs like Hibernate and EclipseLink.

This article is about how to build a database-query with the Criteria API. Please refer to my previous articles about JPA to find Java sources that may not be listed here.

Steps

Simple

Here is a very simple query, just selecting all records from database-table Team:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Team> query = builder.createQuery(Team.class);
query.from(Team.class);
TypedQuery<Team> typedQuery = entityManager.createQuery(query);
return typedQuery.getResultList();

Minimal steps are:

  1. get a CriteriaBuilder from JPA EntityManager
  2. get a CriteriaQuery from CriteriaBuilder
  3. define a Root by calling from() on the CriteriaQuery
  4. wrap a TypedQuery from EntityManager around the CriteriaQuery
  5. fetch the result using one of the TypedQuery methods

Compared to the according JPQL-query

SELECT t from Team t
this is quite elaborate. But it was just the start!-)

Complex

Following query joins three tables, sets up a WHERE-condition using wildcards, and builds explicit return-objects (DTOs).

  1. Using the EntityManager, create a criteria-builder:

    EntityManager entityManager = ....;
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    
  2. Using the builder, create a criteria-query, targeting a result type (here it is an explicit data-transfer-object, not an entity-type):

    CriteriaQuery<ViewDto> query = builder.createQuery(ViewDto.class);
    
  3. Using the query, create a from-root, targeting the main entity class, and add all needed joins:

    Root<Team> teamRoot = query.from(Team.class);
    Join<Team,Responsibility> responsibilityJoin = teamRoot.joinSet("responsibilities");
    Join<Responsibility,Person> personJoin = responsibilityJoin.join("person");
    
  4. Using the builder, create a SELECT clause, containing all needed attributes, or just an entity object (here it is the construction of the DTO)

    CompoundSelection<ViewDto> selectionConstructor = builder.construct(
                    ViewDto.class, 
                    teamRoot, 
                    responsibilityJoin.get("name"), 
                    personJoin.get("name"));
    query.select(selectionConstructor);
    
  5. Using the builder, create WHERE conditions ("%" is the wildcard for LIKE):

    Predicate whereCondition = builder.and(
                    builder.like(responsibilityJoin.get("name"), "%"+responsibilityName+"%"),
                    builder.like(personJoin.get("name"), "%"+personName+"%"));
    query.where(whereCondition);
    
  6. Using the EntityManager, wrap a typed query around the built criteria-query:

    TypedQuery<ViewDto> typedQuery = entityManager.createQuery(query);
    
  7. Call a result-method on the typed query, e.g. getResultList():

    return typedQuery.getResultList();
    

This resembles following untyped JPQL query:

SELECT 
    new fri.jpa.example.TeamDao$ViewDto(t, r.name, p.name) 
FROM Team t 
    JOIN t.responsibilities r 
    JOIN r.person p 
WHERE 
    r.name like :responsibilityName  
    and p.name like :personName

The ViewDto is a static inner class of fri.jpa.example.TeamDao, the '$' replaces the '.' inside the fully qualified class name in SELECT clause. The :responsibilityName and :personName in WHERE clause are named parameter placeholders.

Example Code

DAO

Here is the DAO containing the complex query:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package fri.jpa.example;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;

public class TeamDao
{
    private final EntityManager em;
    
    public TeamDao(EntityManager em) {
        this.em = em;
    }
    
    
    public static class ViewDto
    {
        public final Team team;
        public final String responsibilityName;
        public final String personName;
        
        public ViewDto(Team team, String responsibilityName, String personName) {
            this.team = team;
            this.responsibilityName = responsibilityName;
            this.personName = personName;
        }
    }
     
    public List<ViewDto> findByResponsibilityAndPersonViaWildcard(
            String responsibilityName, 
            String personName)
    {
        final CriteriaBuilder builder = em.getCriteriaBuilder();
        final CriteriaQuery<ViewDto> query = builder.createQuery(ViewDto.class);
        
        final Root<Team> teamRoot = query.from(Team.class);
        final Join<Team,Responsibility> responsibilityJoin = teamRoot.joinSet("responsibilities");
        final Join<Responsibility,Person> personJoin = responsibilityJoin.join("person");
        
        final CompoundSelection<ViewDto> selectionConstructor = builder.construct(
                ViewDto.class, 
                teamRoot, 
                responsibilityJoin.get("name"), 
                personJoin.get("name"));
        query.select(selectionConstructor);
        
        final Predicate whereCondition = builder.and(
                builder.like(responsibilityJoin.get("name"), "%"+responsibilityName+"%"),
                builder.like(personJoin.get("name"), "%"+personName+"%"));
        query.where(whereCondition);
        
        final TypedQuery<ViewDto> typedQuery = em.createQuery(query);
        return typedQuery.getResultList();
    }
}

Unit Test

Following is the unit test for the query.
Please refer to my previous articles about JPA to find out how you can use this super-class to test both Hibernate and EclipseLink.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
package fri.jpa.example;

import static org.junit.Assert.*;
import java.util.*;
import java.util.function.Consumer;
import javax.persistence.*;
import org.junit.*;
import fri.jpa.example.TeamDao.ViewDto;

public abstract class JpaTest
{
    private EntityManager em;

    @Test
    public void findTeamsByResponsibilityAndPersonViaWildcard() {
        final Person peter = transactional(em::persist, newPerson("Peter"));
        final Person paul = transactional(em::persist, newPerson("Paul"));
        final Person mary = transactional(em::persist, newPerson("Mary"));
        
        final Team developmentTeam = new Team();
        final Responsibility developer = newResponsibility("Developer", peter);
        developmentTeam.getResponsibilities().add(developer);
        final Responsibility operator = newResponsibility("Operator", paul);
        developmentTeam.getResponsibilities().add(operator);
        final Responsibility devops = newResponsibility("DevOps", mary);
        developmentTeam.getResponsibilities().add(devops);
        transactional(em::persist, developmentTeam);
        
        final Team managerTeam = new Team();
        final Responsibility developmentManager = newResponsibility("DevelopmentManager", mary);
        managerTeam.getResponsibilities().add(developmentManager);
        final Responsibility roomManager = newResponsibility("RoomManager", paul);
        managerTeam.getResponsibilities().add(roomManager);
        transactional(em::persist, managerTeam);
        
        final TeamDao teamDao = new TeamDao(em);
        final List<ViewDto> result = teamDao.findByResponsibilityAndPersonViaWildcard("Dev", "r");
        // "Dev" is in "Developer" and "DevOps", "r" is in "Peter" and "Mary"
        
        assertEquals(3, result.size());
        
        final ViewDto developerView = result.stream()
                .filter(v -> v.responsibilityName.equals(developer.getName()))
                .findFirst()
                .get();
        assertNotNull(developerView);
        assertEquals(developmentTeam, developerView.team);
        assertEquals(peter.getName(), developerView.personName);
        
        final ViewDto devopsView = result.stream()
                .filter(v -> v.responsibilityName.equals(devops.getName()))
                .findFirst()
                .get();
        assertNotNull(devopsView);
        assertEquals(developmentTeam, devopsView.team);
        assertEquals(mary.getName(), devopsView.personName);
        
        final ViewDto managerView = result.stream()
                .filter(v -> v.responsibilityName.equals(developmentManager.getName()))
                .findFirst()
                .get();
        assertNotNull(managerView);
        assertEquals(managerTeam, managerView.team);
        assertEquals(mary.getName(), managerView.personName);
    }

    private Person newPerson(String name) {
        final Person person = new Person();
        person.setName(name);
        return person;
    }

    private Responsibility newResponsibility(String name, Person person) {
        final Responsibility responsibility = new Responsibility();
        responsibility.setName(name);
        responsibility.setPerson(person);
        return responsibility;
    }

    private <P> P transactional(Consumer<P> entityManagerFunction, P parameter) {
        final EntityTransaction transaction = em.getTransaction();
        try {
            transaction.begin();
            entityManagerFunction.accept(parameter);
            transaction.commit();
            return parameter;
        }
        catch (Throwable th)    {
            th.printStackTrace();
            transaction.rollback();
            throw th;
        }
    }

    /** @return the name of the persistence-unit to use for all tests. */
    protected abstract String getPersistenceUnitName();
    
    @Before
    public void setUp() {
        em = Persistence.createEntityManagerFactory(getPersistenceUnitName()).createEntityManager();
    }
    
    @After
    public void tearDown()  {
        for (Team team : findTeams())
            transactional(em::remove, team);
        for (Responsibility responsibility : findResponsibilities())
            transactional(em::remove, responsibility);
        for (Person person : findPersons())
            transactional(em::remove, person);
    }
}

Conclusion

The JPA Criteria API is complex. But it is strongly typed, and that's a big advantage over String queries, even when these are shorter. Mind that there are also CriteriaUpdate and CriteriaDelete classes beside CriteriaQuery, to be used for UPDATE and DELETE statements.




Keine Kommentare: