Un guide de Jdbi

1. Introduction

Dans cet article, nous allons voir comment interroger une base de données relationnelle avec jdbi.

Jdbi est une bibliothèque Java open source (licence Apache) qui utilise des expressions lambda et une réflexion pour fournir une interface plus conviviale et de niveau supérieur que JDBC pour accéder à la base de données.

Jdbi, cependant, n'est pas un ORM; même s'il a un module optionnel de mappage d'objets SQL, il n'a pas de session avec des objets attachés, une couche d'indépendance de la base de données et d'autres cloches et sifflets d'un ORM typique.

2. Configuration Jdbi

Jdbi est organisé en un noyau et plusieurs modules optionnels.

Pour commencer, il suffit d'inclure le module principal dans nos dépendances:

  org.jdbi jdbi3-core 3.1.0  

Au cours de cet article, nous montrerons des exemples utilisant la base de données HSQL:

 org.hsqldb hsqldb 2.4.0 test 

Nous pouvons trouver la dernière version de jdbi3-core , HSQLDB et les autres modules Jdbi sur Maven Central.

3. Connexion à la base de données

Tout d'abord, nous devons nous connecter à la base de données. Pour ce faire, nous devons spécifier les paramètres de connexion.

Le point de départ est la classe Jdbi :

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

Ici, nous spécifions l'URL de connexion, un nom d'utilisateur et, bien sûr, un mot de passe.

3.1. Paramètres supplémentaires

Si nous devons fournir d'autres paramètres, nous utilisons une méthode surchargée acceptant un objet Properties :

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

Dans ces exemples, nous avons enregistré l' instance Jdbi dans une variable locale. C'est parce que nous allons l'utiliser pour envoyer des déclarations et des requêtes à la base de données.

En fait, le simple fait d'appeler create n'établit aucune connexion avec la base de données. Il enregistre simplement les paramètres de connexion pour plus tard.

3.2. Utilisation d'un DataSource

Si nous nous connectons à la base de données à l'aide d'un DataSource , comme c'est généralement le cas, nous pouvons utiliser la surcharge de création appropriée :

Jdbi jdbi = Jdbi.create(datasource);

3.3. Travailler avec des poignées

Les connexions réelles à la base de données sont représentées par des instances de la classe Handle .

Le moyen le plus simple de travailler avec des poignées et de les fermer automatiquement consiste à utiliser des expressions lambda:

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

Nous appelons useHandle lorsque nous n'avons pas à renvoyer de valeur.

Sinon, nous utilisons withHandle :

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

Il est également possible, bien que non recommandé, d'ouvrir manuellement une poignée de connexion; dans ce cas, nous devons le fermer lorsque nous avons terminé:

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

Heureusement, comme nous pouvons le voir, Handle implémente Closeable , donc il peut être utilisé avec try-with-resources.

4. Déclarations simples

Maintenant que nous savons comment obtenir une connexion, voyons comment l'utiliser.

Dans cette section, nous allons créer un tableau simple que nous utiliserons tout au long de l'article.

Pour envoyer des instructions telles que create table à la base de données, nous utilisons la méthode execute :

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

execute renvoie le nombre de lignes affectées par l'instruction:

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

En fait, exécuter n'est qu'une méthode pratique.

Nous examinerons des cas d'utilisation plus complexes dans les sections suivantes, mais avant de faire cela, nous devons apprendre à extraire les résultats de la base de données.

5. Interrogation de la base de données

L'expression la plus simple qui produit des résultats à partir de la base de données est une requête SQL.

Pour émettre une requête avec un handle Jdbi, nous devons au moins:

  1. créer la requête
  2. choisissez comment représenter chaque ligne
  3. itérer sur les résultats

Nous allons maintenant examiner chacun des points ci-dessus.

5.1. Créer une requête

Sans surprise, Jdbi représente les requêtes comme des instances de la classe Query .

Nous pouvons en obtenir un à partir d'une poignée:

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

5.2. Cartographie des résultats

Jdbi fait abstraction du JDBC ResultSet , qui possède une API assez lourde.

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'implémentation de tous ces exemples et extraits de code se trouve dans le projet GitHub - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.