1. Introducción
En este artículo, veremos cómo consultar una base de datos relacional con jdbi.
Jdbi es una biblioteca Java de código abierto (licencia Apache) que utiliza expresiones lambda y reflexión para proporcionar una interfaz de nivel superior más amigable que JDBC para acceder a la base de datos.
Jdbi, sin embargo, no es un ORM; a pesar de que tiene un módulo de mapeo de objetos SQL opcional, no tiene una sesión con objetos adjuntos, una capa de independencia de la base de datos y otras características de un ORM típico.
2. Configuración de Jdbi
Jdbi está organizado en un núcleo y varios módulos opcionales.
Para empezar, solo tenemos que incluir el módulo principal en nuestras dependencias:
org.jdbi jdbi3-core 3.1.0
En el transcurso de este artículo, mostraremos ejemplos utilizando la base de datos HSQL:
org.hsqldb hsqldb 2.4.0 test
Podemos encontrar la última versión de jdbi3-core , HSQLDB y los otros módulos de Jdbi en Maven Central.
3. Conexión a la base de datos
Primero, necesitamos conectarnos a la base de datos. Para hacer eso, tenemos que especificar los parámetros de conexión.
El punto de partida es la clase Jdbi :
Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", "");
Aquí, especificamos la URL de conexión, un nombre de usuario y, por supuesto, una contraseña.
3.1. Parámetros adicionales
Si necesitamos proporcionar otros parámetros, usamos un método sobrecargado que acepta un objeto de Propiedades :
Properties properties = new Properties(); properties.setProperty("username", "sa"); properties.setProperty("password", ""); Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", properties);
En estos ejemplos, hemos guardado la instancia de Jdbi en una variable local. Eso es porque lo usaremos para enviar declaraciones y consultas a la base de datos.
De hecho, simplemente llamar a create no establece ninguna conexión con la base de datos. Simplemente guarda los parámetros de conexión para más adelante.
3.2. Usando una fuente de datos
Si nos conectamos a la base de datos usando un DataSource , como suele ser el caso, podemos usar la sobrecarga de creación adecuada :
Jdbi jdbi = Jdbi.create(datasource);
3.3. Trabajar con asas
Las conexiones reales a la base de datos están representadas por instancias de la clase Handle .
La forma más sencilla de trabajar con identificadores y hacer que se cierren automáticamente es mediante el uso de expresiones lambda:
jdbi.useHandle(handle -> { doStuffWith(handle); });
Llamamos useHandle cuando no tenemos que devolver un valor.
De lo contrario, usamos withHandle :
jdbi.withHandle(handle -> { return computeValue(handle); });
También es posible, aunque no recomendado, abrir manualmente un identificador de conexión; en ese caso, tenemos que cerrarlo cuando hayamos terminado:
Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", ""); try (Handle handle = jdbi.open()) { doStuffWith(handle); }
Afortunadamente, como podemos ver, Handle implementa Closeable , por lo que se puede usar con try-with-resources.
4. Declaraciones simples
Ahora que sabemos cómo obtener una conexión, veamos cómo usarla.
En esta sección, crearemos una tabla simple que usaremos a lo largo del artículo.
Para enviar declaraciones como crear tabla a la base de datos, usamos el método de ejecución :
handle.execute( "create table project " + "(id integer identity, name varchar(50), url varchar(100))");
ejecutar devuelve el número de filas que fueron afectadas por la declaración:
int updateCount = handle.execute( "insert into project values " + "(1, 'tutorials', 'github.com/eugenp/tutorials')"); assertEquals(1, updateCount);
En realidad, ejecutar es solo un método de conveniencia.
Veremos casos de uso más complejos en secciones posteriores, pero antes de hacer eso, necesitamos aprender cómo extraer resultados de la base de datos.
5. Consultar la base de datos
La expresión más sencilla que produce resultados a partir de la base de datos es una consulta SQL.
Para emitir una consulta con un identificador Jdbi, tenemos que, al menos:
- crear la consulta
- elige cómo representar cada fila
- iterar sobre los resultados
Ahora veremos cada uno de los puntos anteriores.
5.1. Crear una consulta
Como era de esperar, Jdbi representa las consultas como instancias de la clase Query .
Podemos obtener uno de un mango:
Query query = handle.createQuery("select * from project");
5.2. Mapeo de los resultados
Jdbi se aleja de JDBC ResultSet , que tiene una API bastante engorrosa.
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
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
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.
La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en el proyecto GitHub; este es un proyecto de Maven, por lo que debería ser fácil de importar y ejecutar como está.