Queries met JPQL

Wij hebben gezien hoe dat wij met find één enkele entiteit konden opvragen op basis van een id, en hoe dat wij entiteiten konden aanmaken en toevoegen, aanpassen en verwijderen.

Wat wij eigenlijk nog niet gezien hebben is hoe dat wij meerdere gegevens kunnen opvragen, of gewoon queries met eigen, custom criteria om specifieke gegevens op te vragen.

In JDBC gingen wij hier werken met query statements en een ResultSet. Ergens lijkt het hier daarop, alleen dat wij:

  1. Ons niet bezig moeten houden met specifieke flavours van SQL, enkel eentje die meer OOP aanvoelt: JPQL
  2. Wij ons niet meer bezig moeten houden met handmatige mapping en zo een object aanmaken en toevoegen aan een lijst: je krijgt gewoon meteen een lijst vol objecten terug – zalig!

Wij gaan eerst de werkwijze laten zien, zodanig dat je een gevoel krijgt van hoe bekend het lijkt.

Ten eerste een overzicht van onze gegevens:

Stel wij willen een simpele ‘retrieveAllBankAccounts’ logica schrijven, dan zullen wij gebruik moeten maken van een query via JPQL.

    public static void main(String[] args) {
        TypedQuery<BankAccount> query = em.createQuery("SELECT b FROM BankAccount b", BankAccount.class);
        List<BankAccount> bankAccounts = query.getResultList();
        bankAccounts.forEach(System.out::println);
    }

Hier gaan wij stap voor stap over. Ten eerste, TypedQuery. Dat is een object die een JPQL query voorstelt en die meteen ook een type heeft (in dit geval BankAccount), zodanig dat wij meteen een lijst van BankAccount’s kunnen opvragen en verwachten. Met een gewone Query moet je gaan casten en dat is totaal niet aangeraden. Hou je dus aan TypedQuery als je al op voorhand weet welke klasse jij verwacht.

Als tweede zien wij dat er een em.createQuery() gebeurt. Het is dus via onze EntityManager dat onze queries worden aangemaakt. Ergens is dat ook logisch, want ergens achter de schermen heeft EntityManager (dankzij EntityManagerFactory) weet van welke flavour van SQL (bv. MySQL of MariaDB of H2) hij naar moet vertalen. Daarom dat een query zich moet baseren op een EntityManager.

Als derde zien wij twee argumenten voor die methode. Ten eerste een soort van SQL statement, en ten tweede een BankAccount.class. Dat eerste is geen SQL maar JPQL (Java Persistence Query Language), en daar komen wij zometeen op terug. Dat tweede is maar een manier om TypedQuery op de hoogte te brengen van de klasse die jij per se terug verwacht.

Die JPQL statement zal je wel bekend voorkomen. Het komt overeen met een “SELECT * FROM BankAccount”, alleen dat wij hier ons bezig houden met klassenamen (‘BankAccount’ in dit geval), en een soort van alias die ook dient als object (in dit geval ‘b’). Hoe dat wij deze kunnen gebruiken of hoe dat het werkt zien wij zometeen wel.

Als vierde zien wij dat wij aan een TypedQuery zomaar een getResultList() kunnen vragen om meteen een lijst met al onze objecten in handen te krijgen. Bovendien zijn ze allemaal managed entities dus je kan meteen aan de slag met CRUD operaties ermee. getResultList is dus overeenkomstig met ResultSet, alleen nog veel aangenamer. Je kan ook vragen voor enkel één resultaat als je maar één resultaat verwacht via getSingleResult(). Let wel op dat je hier een exception krijgt als je meer dan enkel één result terugkrijgt (logisch ook want dan betekent het dat jouw query niet doet wat jij had verwacht en dat je daar langer stil bij moet staan).

Als laatste printen wij al onze opgevraagde gegevens via een stream.

Deze kunnen wij dus in een aparte methode plaatsen als refactor voor de leesbaarheid en herbruikbaardheid:

    public static void main(String[] args) {
        List<BankAccount> bankAccounts = retrieveAll();
        bankAccounts.forEach(System.out::println);
    }
    private static List<BankAccount> retrieveAll(){
        TypedQuery<BankAccount> query = em.createQuery("SELECT b FROM BankAccount b", BankAccount.class);
        return query.getResultList();
    }

Als je criteria wil toevoegen, bij voorbeeld alle bankrekeningen met meer dan 100.000 als saldo, kan het als volgt:

    public static void main(String[] args) {
        TypedQuery<BankAccount> query = em.createQuery("SELECT b FROM BankAccount b WHERE b.balance > 100000", BankAccount.class);
        List<BankAccount> bankAccounts =  query.getResultList();
        bankAccounts.forEach(System.out::println);
    }

Het lijkt hard op wat SQL-kenners kennen, alleen met een OOP-toets.

Bovendien kan het dynamisch (net zoals bij JDBC maar net iets anders):

    public static void main(String[] args) {
        List<BankAccount> bankAccounts =  retrieveBankAccountsWithMinBalance(100_000);
        bankAccounts.forEach(System.out::println);
    }
    private static List<BankAccount> retrieveBankAccountsWithMinBalance(double minBalance){
        TypedQuery<BankAccount> query = em.createQuery("SELECT b FROM BankAccount b WHERE b.balance > ?1", BankAccount.class);
        query.setParameter(1, minBalance);
        return query.getResultList();
    }

Het is net iets leesbaarder dan JDBC want er staat nu een getal om het gemakkelijker te maken om te lezen tijdens invulling. Bovendien heb je een generische ‘setParameter’ nu die alle datatypes aanvaardt (in plaats van setInt, setString, etc.).

Nog leuker dan vraagtekens zijn Named parameters:

    public static void main(String[] args) {
        List<BankAccount> bankAccounts =  retrieveBankAccountsWithMinBalance(100_000);
        bankAccounts.forEach(System.out::println);
    }
    private static List<BankAccount> retrieveBankAccountsWithMinBalance(double minBalance){
        TypedQuery<BankAccount> query = em.createQuery("SELECT b FROM BankAccount b WHERE b.balance > :minBalance", BankAccount.class);
        query.setParameter("minBalance", minBalance);
        return query.getResultList();
    }

Nu is het nog leesbaarder want jouw parameter heeft nu een naam. Veel duidelijker om aan te spreken dan een van de vele mysterieuze vraagtekens bij JDBC!