Jdbi, som JDBC borde vara

Jdbi

Idag undersöker vi Jdbi, ett lättviktigt ramverk byggt ovanpå JDBC. Framtaget för att förbättra JDBCs råa interface och förenkla kommunikationen med en relationsdatabas. Ramverket erbjuder legobitar för dig som utvecklare att nyttja för att skapa precis den mappning mellan objekt och relationer som passar just din applikation. Till skillnad från kompletta Object Relational Mapping ramverk så är det klassisk SQL som skrivs för att jobba mot DBn. Vidare kan noteras att storleken på vår exempeltjänst gick från 18 680 kB med Hibernate-core till 8 046 kB med jdbi3-core. En signifikant reducering i jar-storlek, något som kanske spelar mindre roll för en monolit men som kan vara väl värt att ta i beaktande när man har en uppsjö av Microservices deployade i molnet.

Uppsättning

Jdbi är modulbaserat och det finns många intressanta moduler att välja bland. Jdbi-core är grunden och det enda som krävs för att komma igång. Ofta räcker grundfunktionerna gott och väl för en enklare Microservice. Av gammal hävd har vi valt att även denna gång jobba mot en PostgreSQL-databas i vårt exempel. Vår pom.xml får således följande beroenden:

<dependencies>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.jdbi</groupId>
        <artifactId>jdbi3-core</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

Ansluta mot databasen

Alla anslutningar mot databasen görs via en instans av Jdbi-klassen. Det finns flera olika create-metoder att välja på beroende på vad man föredrar. Nedan skickar vi in hårdkodade strängar, fast det går även bra att skicka in ett Java Properties-objekt eller varför inte en Datasource.

var jdbi = Jdbi.create("jdbc:postgresql://db:5432/dummy", "myUser", "myPwd");

Jdbi-instansen är trådsäker och att skapa en instans ansluter inte Jdbi mot DBn. Instansen sparar bara anslutningsinformationen för framtida användning. Best practice är att ha en delad Jdbi-instans per Microservice enligt klassisk Singleton-princip.

När det väl blir dags att göra anrop mot databasen så efterfrågar man Handle-objekt från Jdbi-instansen. Detta görs huvudsakligen via något av följande två sätt:

I det fall SQL-satsen förväntas returnera något används jdbi.withHandle():

List<String> tags = jdbi.withHandle(handle ->
    handle.createQuery("SELECT name FROM persons ORDER BY id").mapTo(String.class).list());

Om inget returvärde förväntas använd istället jdbi.useHandle():

jdbi.useHandle(handle ->
    handle.execute("INSERT INTO persons (name) values (?)", person.name));

Med det sagt är grunden satt. Dags att börja använda Jdbi!

Använda Jdbi

Vi börjar med att titta en extra gång på vad vi gjorde i withHandle()-koden ovan.

createQuery(“SELECT name FROM persons ORDER BY id”) skapade ett Query-objekt där vi efterfrågar namnet på alla personer i persons-tabellen.
.mapTo(String.class) mappade resultatet (namnen) till String-objekt.
.list() ackumulerade sedan alla träffar i en lista av strängar som sedan returneras.

Utöver list() finns flera andra funktioner, t.ex.:
findFirst() som gör att den första raden i resultatet som uppfyller kriterierna returneras. Metoden returnerar en Optional då det är möjligt att ingen match ges.

findOnly() vilket används i det fall man vill ha ett och endast ett svar, typfallet är att hämta upp en post i DBn baserat på dess nyckel ala “SELECT name FROM persons WHERE id = 1”. Om inget resultat eller mer än ett påträffas kastar findOnly() ett IllegalStateException.

Det finns även andra alternativ så som att strömma resultatet

handle.createQuery("SELECT name FROM tags ORDER BY id").mapTo(String.class).useStream(stream -> {
    // do something awesome
});

För att sända förändringskommandon till databasen använder vi execute.

int affectedRows = handle.execute("INSERT INTO persons (name) values (?)", person.name));

Kommandot skickar tillbaka en integer som låter dig veta hur många rader som påverkats av kommandot, t.ex. 1 i fallet med inserten ovan.

Mappning

Ganska snart når man ett läge där man vill mappa sina POJOs mot datat i databasen. Jdbi har tre olika varianter av Mappers: Row Mappers, Column Mappers samt Reflection Mappers. Vi kommer här titta på den förstnämnda, d.v.s. Row Mappers.

Man utgår från RowMapper-interfacet som rent praktiskt mappar den aktuella raden i “JDBC-resultsetet” mot en mappad typ. RowMapper kan tillhandahållas “inline” genom att använda ett lambda-uttryck:

List<Person> persons = handle.createQuery("SELECT id, name FROM persons ORDER BY id ASC")
    .map((rs, ctx) -> new User(rs.getInt("id"), rs.getString("name")))
    .list();

Där Rs är ett ResultSet och ctx ett StatementContextm, vilket vi kommer se tydligt när vi istället skapar en PersonMapper-klass för att underlätta återanvändbarheten (och läsbarheten).

import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

import java.sql.ResultSet;
import java.sql.SQLException;

Public class PersonMapper implements RowMapper<Person> {
    @Override
    public Person map(ResultSet rs, StatementContext ctx) throws SQLException {
        return new Person(rs.getInt("id"), rs.getString("name"));
    }
}
List<Person> persons = handle.createQuery("SELECT id, name FROM persons ORDER BY id ASC")
    .map(new PersonMapper())
    .list();

Transaktioner

Om ni behöver uppdatera flera tabeller eller av någon annan anledning köra flera olika SQL-satser som en atomisk operation så kan vi glädja er med att Jdbi har fullt stöd för JDBC-transaktioner.

Precis som med handles finns det två primära varianter av transaktions-startande anrop. inTransaction() används i det fall vi vill returnera något från vår closure, annars använd useTransaction().

List<Person> persons = jdbi.withHandle(handle -> handle.inTransaction(h -> {
    h.execute("UPDATE persons SET name = 'Exertus' WHERE id=1337");
    return h.createQuery("SELECT * FROM persons ORDER BY id").map(new PersonMapper()).list();
}));

Vi behöver inte själva commita-transaktionen utan det gör Jdbi automatiskt när den inre funktionen exekverat klart. Om något går snett så kommer allt inom transaktionen rullas tillbaka och ett lämpligt Exception kastas.

Sammanfattning

Med den här introduktionsguiden hoppas vi att ni fått en bild av hur ni skulle kunna komma igång med Jdbi. Det finns en uppsjö av ytterligare funktioner och tilläggsmoduler att använda helt efter tycke och smak. Vi rekommenderar den vetgirige att läsa mer på Jdbis egna hemsida.

Kittlar den här typen av lösningar ert kreativa sinne eller vill ni kanske bara träffas över en fika för att diskutera möjligheter? Hör då av dig till oss via formuläret nedan!

Vi har mottagit ditt meddelande och återkommer inom kort.
Hoppsan! Något gick dessvärre fel, vänligen verifiera att du inte är en robot eller ladda om sidan och försök igen.
Vi vill göra dig uppmärksam på att vi behandlar dina uppgifter i strikt enlighet med vår Integritetspolicy. Allt för att du ska känna dig trygg i att vi värnar om din integritet.