Una guida a Jdbi

1. Introduzione

In questo articolo, vedremo come interrogare un database relazionale con jdbi.

Jdbi è una libreria Java open source (licenza Apache) che utilizza espressioni lambda e reflection per fornire un'interfaccia più amichevole e di livello superiore rispetto a JDBC per accedere al database.

Jdbi, tuttavia, non è un ORM; anche se ha un modulo di mappatura oggetti SQL opzionale, non ha una sessione con oggetti allegati, un livello di indipendenza dal database e qualsiasi altro campanello e fischio di un tipico ORM.

2. Configurazione Jdbi

Jdbi è organizzato in un nucleo e diversi moduli opzionali.

Per iniziare, dobbiamo solo includere il modulo principale nelle nostre dipendenze:

  org.jdbi jdbi3-core 3.1.0  

Nel corso di questo articolo, mostreremo esempi che utilizzano il database HSQL:

 org.hsqldb hsqldb 2.4.0 test 

Possiamo trovare l'ultima versione di jdbi3-core , HSQLDB e gli altri moduli Jdbi su Maven Central.

3. Connessione al database

Innanzitutto, dobbiamo connetterci al database. Per fare ciò, dobbiamo specificare i parametri di connessione.

Il punto di partenza è la classe Jdbi :

Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", "");

Qui stiamo specificando l'URL di connessione, un nome utente e, ovviamente, una password.

3.1. Parametri aggiuntivi

Se dobbiamo fornire altri parametri, utilizziamo un metodo sovraccarico che accetta un oggetto Properties :

Properties properties = new Properties(); properties.setProperty("username", "sa"); properties.setProperty("password", ""); Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", properties);

In questi esempi, abbiamo salvato l' istanza Jdbi in una variabile locale. Questo perché lo useremo per inviare istruzioni e query al database.

Infatti, la semplice chiamata a create non stabilisce alcuna connessione al DB. Salva solo i parametri di connessione per dopo.

3.2. Utilizzando un DataSource

Se ci colleghiamo al database utilizzando un DataSource , come di solito accade, possiamo utilizzare l'appropriato create overload:

Jdbi jdbi = Jdbi.create(datasource);

3.3. Lavorare con le maniglie

Le connessioni effettive al database sono rappresentate da istanze della classe Handle .

Il modo più semplice per lavorare con gli handle e chiuderli automaticamente è usare le espressioni lambda:

jdbi.useHandle(handle -> { doStuffWith(handle); });

Chiamiamo useHandle quando non dobbiamo restituire un valore.

Altrimenti, usiamo withHandle :

jdbi.withHandle(handle -> { return computeValue(handle); });

È anche possibile, sebbene non consigliato, aprire manualmente un handle di connessione; in tal caso, dobbiamo chiuderlo quando abbiamo finito:

Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", ""); try (Handle handle = jdbi.open()) { doStuffWith(handle); }

Fortunatamente, come possiamo vedere, Handle implementa Closeable , quindi può essere utilizzato con try-with-resources.

4. Dichiarazioni semplici

Ora che sappiamo come ottenere una connessione vediamo come usarla.

In questa sezione creeremo una semplice tabella che useremo in tutto l'articolo.

Per inviare istruzioni come create table al database, utilizziamo il metodo execute :

handle.execute( "create table project " + "(id integer identity, name varchar(50), url varchar(100))");

execute restituisce il numero di righe che sono state influenzate dall'istruzione:

int updateCount = handle.execute( "insert into project values " + "(1, 'tutorials', 'github.com/eugenp/tutorials')"); assertEquals(1, updateCount);

In realtà, l'esecuzione è solo un metodo conveniente.

Esamineremo casi d'uso più complessi nelle sezioni successive, ma prima di farlo, dobbiamo imparare come estrarre i risultati dal database.

5. Interrogazione del database

L'espressione più semplice che produce risultati dal database è una query SQL.

Per inviare una query con un handle Jdbi, dobbiamo almeno:

  1. creare la query
  2. scegli come rappresentare ogni riga
  3. iterare sui risultati

Ora esamineremo ciascuno dei punti sopra.

5.1. Creazione di una query

Non sorprende che Jdbi rappresenti le query come istanze della classe Query .

Possiamo ottenerne uno da una maniglia:

Query query = handle.createQuery("select * from project");

5.2. Mappatura dei risultati

Jdbi astrae dal ResultSet JDBC , che ha un'API piuttosto ingombrante.

Therefore, it offers several possibilities to access the columns resulting from a query or some other statement that returns a result. We'll now see the simplest ones.

We can represent each row as a map:

query.mapToMap();

The keys of the map will be the selected column names.

Or, when a query returns a single column, we can map it to the desired Java type:

handle.createQuery("select name from project").mapTo(String.class);

Jdbi has built-in mappers for many common classes. Those that are specific to some library or database system are provided in separate modules.

Of course, we can also define and register our mappers. We'll talk about it in a later section.

Finally, we can map rows to a bean or some other custom class. Again, we'll see the more advanced options in a dedicated section.

5.3. Iterating Over the Results

Once we've decided how to map the results by calling the appropriate method, we receive a ResultIterable object.

We can then use it to iterate over the results, one row at a time.

Here we'll look at the most common options.

We can merely accumulate the results in a list:

List results = query.mapToMap().list();

Or to another Collection type:

List results = query.mapTo(String.class).collect(Collectors.toSet());

Or we can iterate over the results as a stream:

query.mapTo(String.class).useStream((Stream stream) -> { doStuffWith(stream) });

Here, we explicitly typed the stream variable for clarity, but it's not necessary to do so.

5.4. Getting a Single Result

As a special case, when we expect or are interested in just one row, we have a couple of dedicated methods available.

If we want at most one result, we can use findFirst:

Optional first = query.mapToMap().findFirst();

As we can see, it returns an Optional value, which is only present if the query returns at least one result.

If the query returns more than one row, only the first is returned.

If instead, we want one and only one result, we use findOnly:

Date onlyResult = query.mapTo(Date.class).findOnly();

Finally, if there are zero results or more than one, findOnly throws an IllegalStateException.

6. Binding Parameters

Often, queries have a fixed portion and a parameterized portion. This has several advantages, including:

  • security: by avoiding string concatenation, we prevent SQL injection
  • ease: we don't have to remember the exact syntax of complex data types such as timestamps
  • performance: the static portion of the query can be parsed once and cached

Jdbi supports both positional and named parameters.

We insert positional parameters as question marks in a query or statement:

Query positionalParamsQuery = handle.createQuery("select * from project where name = ?");

Named parameters, instead, start with a colon:

Query namedParamsQuery = handle.createQuery("select * from project where url like :pattern");

In either case, to set the value of a parameter, we use one of the variants of the bind method:

positionalParamsQuery.bind(0, "tutorials"); namedParamsQuery.bind("pattern", "%github.com/eugenp/%");

Note that, unlike JDBC, indexes start at 0.

6.1. Binding Multiple Named Parameters at Once

We can also bind multiple named parameters together using an object.

Let's say we have this simple query:

Query query = handle.createQuery( "select id from project where name = :name and url = :url"); Map params = new HashMap(); params.put("name", "REST with Spring"); params.put("url", "github.com/eugenp/REST-With-Spring");

Then, for example, we can use a map:

query.bindMap(params);

Or we can use an object in various ways. Here, for example, we bind an object that follows the JavaBean convention:

query.bindBean(paramsBean);

But we could also bind an object's fields or methods; for all the supported options, see the Jdbi documentation.

7. Issuing More Complex Statements

Now that we've seen queries, values, and parameters, we can go back to statements and apply the same knowledge.

Recall that the execute method we saw earlier is just a handy shortcut.

In fact, similarly to queries, DDL and DML statements are represented as instances of the class Update.

We can obtain one by calling the method createUpdate on a handle:

Update update = handle.createUpdate( "INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)");

Then, on an Update we have all the binding methods that we have in a Query, so section 6. applies for updates as well.url

Statements are executed when we call, surprise, execute:

int rows = update.execute();

As we have already seen, it returns the number of affected rows.

7.1. Extracting Auto-Increment Column Values

As a special case, when we have an insert statement with auto-generated columns (typically auto-increment or sequences), we may want to obtain the generated values.

Then, we don't call execute, but executeAndReturnGeneratedKeys:

Update update = handle.createUpdate( "INSERT INTO PROJECT (NAME, URL) " + "VALUES ('tutorials', 'github.com/eugenp/tutorials')"); ResultBearing generatedKeys = update.executeAndReturnGeneratedKeys();

ResultBearing is the same interface implemented by the Query class that we've seen previously, so we already know how to use it:

generatedKeys.mapToMap() .findOnly().get("id");

8. Transactions

We need a transaction whenever we have to execute multiple statements as a single, atomic operation.

As with connection handles, we introduce a transaction by calling a method with a closure:

handle.useTransaction((Handle h) -> { haveFunWith(h); });

And, as with handles, the transaction is automatically closed when the closure returns.

However, we must commit or rollback the transaction before returning:

handle.useTransaction((Handle h) -> { h.execute("..."); h.commit(); });

If, however, an exception is thrown from the closure, Jdbi automatically rolls back the transaction.

As with handles, we have a dedicated method, inTransaction, if we want to return something from the closure:

handle.inTransaction((Handle h) -> { h.execute("..."); h.commit(); return true; });

8.1. Manual Transaction Management

Although in the general case it's not recommended, we can also begin and close a transaction manually:

handle.begin(); // ... handle.commit(); handle.close();

9. Conclusions and Further Reading

In this tutorial, we've introduced the core of Jdbi: queries, statements, and transactions.

We've left out some advanced features, like custom row and column mapping and batch processing.

We also haven't discussed any of the optional modules, most notably the SQL Object extension.

Everything is presented in detail in the Jdbi documentation.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata nel progetto GitHub: questo è un progetto Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.