DatabaseClient es la clase central en el paquete principal R2DBC. Maneja la creación y liberación de recursos, lo que ayuda a evitar errores comunes como olvidar cerrar una conexión. Realiza las tareas básicas del flujo de trabajo principal de R2DBC (como crear y ejecutar declaraciones), dejando que el código de la aplicación proporcione SQL y recupere los resultados. Clase DatabaseClient:

  • Ejecuta consultas SQL

  • Actualiza declaraciones y llamadas a procedimientos almacenados

  • Atraviesa instancias de Result

  • Captura excepciones R2DBC y las transforma en una jerarquía de excepciones escrita y más significativa. definido en el paquete org.springframework.dao. (Ver Jerarquía consistente de excepciones)

El cliente tiene una API rica y gratuita que utiliza tipos reactivos para la composición declarativa.

Si usa DatabaseClient para su código, solo necesita implementar java.util.functioninterfaces proporcionándoles un contrato claramente definido. Dada una Connection proporcionada por una clase DatabaseClient, la devolución de llamada Function crea un Publisher. Lo mismo ocurre con las funciones de visualización que recuperan el resultado de Row.

Puedes usar DatabaseClient dentro de una implementación DAO creando una instancia directamente con una referencia a ConnectionFactory, o puede configurarlo en el contenedor Spring IoC y pasarlo a objetos DAO como referencia de bean.

La forma más sencilla de crear un DatabaseClient el objeto es un método de fábrica estático como se muestra a continuación:

Java
Cliente DatabaseClient = DatabaseClient.create(connectionFactory);
Kotlin
val client = DatabaseClient.create(connectionFactory)
ConnectionFactory siempre debe configurarse como un bean en el contenedor Spring IoC.

El método anterior crea un DatabaseClient con la configuración predeterminada.

También puede obtener una instancia de Builder desde DatabaseClient.builder(). Puede configurar el cliente llamando a los siguientes métodos:

  • ….bindMarkers(…): especifique un BindMarkersFactory específico para configure un parámetro con nombre para convertir tokens de enlace de base de datos.

  • ….executeFunction(…): establezca ExecuteFunction según qué objetos Statement se ejecutarán.

  • ....namedParameters(false): deshabilita la expansión de parámetros con nombre. Habilitado de forma predeterminada.

Los dialectos están determinados por BindMarkersFactoryResolver de ConnectionFactory , normalmente marcando ConnectionFactoryMetadata. Puedes permitir que Spring descubra automáticamente tu BindMarkersFactory registrando una clase que implemente org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider a través de META-INF/spring.factories. BindMarkersFactoryResolver descubre implementaciones de proveedores de marcadores de enlace desde el classpath usando SpringFactoriesLoader.

Actualmente se admiten las siguientes bases de datos:

  • H2

  • MariaDB

  • Microsoft SQL Server

  • MySQL

  • Postgres

Todas las consultas SQL emitidas por esta clase se registran en el nivel DEBUG bajo la categoría correspondiente al nombre de clase completo de la instancia del cliente (normalmente DefaultDatabaseClient) . Además, cada ejecución registra un punto de interrupción en una secuencia reactiva para ayudar en la depuración.

Las siguientes secciones proporcionan algunos ejemplos del uso de DatabaseClient. Estos ejemplos no representan una lista exhaustiva de todas las funciones proporcionadas por DatabaseClient. Consulte el complemento javadoc .

Ejecución de declaraciones

DatabaseClient proporciona una funcionalidad básica de ejecución de declaraciones. El siguiente ejemplo muestra lo que se debe incluir en el código mínimo pero completamente funcional que crea una nueva tabla:

Java

Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .then();
Kotlin

client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .await()

DatabaseClient está diseñado para un uso cómodo y gratuito. Expone métodos intermedios, de continuación y finales en cada etapa de la especificación de ejecución. El ejemplo anterior utiliza then() para devolver un Publisher de terminación que se completa tan pronto como se completa la consulta (o consultas, si la consulta SQL contiene varias declaraciones).

execute(…) acepta una cadena de consulta SQL o una consulta Supplier<String> para diferir la creación real de la consulta antes de ejecutarla.

Construcción de consultas (SELECT)

Las consultas SQL pueden devolver valores a través de Row objetos o número de filas afectadas. DatabaseClient puede devolver el número de filas actualizadas o las filas mismas, dependiendo de la consulta emitida.

La siguiente consulta recupera las columnas id y name de la tabla:

Java

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
        .fetch().first();
Kotlin

val first = client.sql("SELECT id, name FROM person")
        .fetch().awaitSingle()

A continuación, la solicitud utiliza una variable vinculante:

Java

Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().first();
Kotlin

val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitSingle()

Es posible que hayas notado el uso de fetch() en el ejemplo anterior. fetch() es una declaración de continuación que le permite especificar cuántos datos recuperar.

Llamar a first() devuelve la primera fila del resultado y descarta las líneas restantes. Los datos se pueden consumir utilizando los siguientes operadores:

  • first() devuelve la primera fila del resultado completo. Su forma de rutina de Kotlin se llama awaitSingle() para valores de retorno que no aceptan valores NULL, y awaitSingleOrNull() si el valor es opcional.

  • one() devuelve exactamente un resultado y falla si el resultado contiene más líneas. Se utilizan corrutinas de Kotlin, concretamente awaitOne() para obtener exactamente un valor o awaitOneOrNull() si el valor puede ser null.

  • all() devuelve todas las filas del resultado. Cuando use corrutinas en Kotlin, use flow().

  • rowsUpdated() devuelve el número de filas afectadas (contador INSERT / UPDATE / DELETE). Su variante de rutina de Kotlin se llama awaitRowsUpdated().

Sin especificar más detalles de mapeo, las consultas devuelven resultados tabulares como un Map, cuyas claves son nombres de columna que no distinguen entre mayúsculas y minúsculas y se asignan a valores de columna.

Puedes controlar la visualización de resultados proporcionando una Function<Row, T> que se llama para cada Row para que pueda devolver valores arbitrarios (individuales, colecciones, mapas y objetos).

El siguiente ejemplo recupera el name columna y produce su valor:

Java

Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("name", String.class))
        .all();
Kotlin

val names = client.sql("SELECT name FROM person")
        .map{ row: Row -> row.get("name", String.class) }
        .flow()
        
¿Qué pasa con null?

Los resultados de la base de datos relacional pueden contener valores null. La especificación Reactive Streams prohíbe pasar valores null. Este requisito implica un manejo adecuado de null en la función de extracción. Aunque es posible obtener valores null de Fila, no es necesario generar un valor null. Debe envolver todos los valores null en un objeto (por ejemplo, Optional para valores únicos) para garantizar que el valor null nunca sea devuelto directamente por la función extractora.

Actualización (INSERT, UPDATE y DELETE) usando DatabaseClient

La única diferencia con las declaraciones de actualización es que estas declaraciones normalmente no devuelven datos de la tabla, por lo que se usa la función rowsUpdated() para obtener los resultados.

El siguiente ejemplo muestra una declaración UPDATE que devuelve el número de filas actualizadas:

Java

Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated()
        
Kotlin

val affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitRowsUpdated()

Vincular valores a consultas

Una aplicación típica requiere declaraciones SQL parametrizadas para recuperar o actualizar filas en función de datos específicos. datos de entrada. Por lo general, se trata de declaraciones SELECT limitadas por una cláusula WHERE, o declaraciones INSERT y UPDATE que aceptan parámetros de entrada. Las declaraciones parametrizadas conllevan el riesgo de inyección SQL si los parámetros no están configurados correctamente. DatabaseClient utiliza la API bind de R2DBC para eliminar el riesgo de inyección SQL para los parámetros de consulta. Puede especificar una instrucción SQL parametrizada utilizando la instrucción execute(...) y vincular los parámetros a la Statement real. Luego, el controlador R2DBC ejecuta la declaración utilizando la declaración compilada y la sustitución de parámetros.

La vinculación de parámetros admite dos estrategias de vinculación:

  • Por índice, utilizando índices de parámetros nulos.

  • Por índice, usando índices de parámetro cero.

  • Por nombre, usando el nombre del marcador de posición.

El siguiente ejemplo muestra la vinculación de parámetros para una solicitud:


db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
    .bind("id", "joe")
    .bind("name", "Joe")
    .bind("age", 34);
marcadores de anclaje nativos de R2DBC

R2DBC utiliza tokens de enlace de base de datos nativos, que dependen del proveedor de la base de datos real. Como ejemplo, Postgres utiliza tokens indexados como $1, $2, $n. Otro ejemplo es SQL Server, que utiliza tokens de enlace con nombre con el prefijo @.

Esto es diferente de JDBC, que requiere ? como tokens de enlace. En JDBC, los controladores reales convierten los tokens de anclaje ? en tokens de base de datos nativos durante la ejecución de la declaración.

El soporte de R2DBC en Spring Framework le permite utilizar tokens de anclaje nativos o tokens con nombre tokens de anclaje con syntax :name.

El soporte de parámetros con nombre utiliza una instancia BindMarkersFactory para extender los parámetros con nombre a marcadores de enlace nativos en el momento de la consulta, proporcionando un grado de portabilidad de consultas entre diferentes fabricantes de bases de datos.

El preprocesador de consultas expande los parámetros con nombre Collection en una serie de tokens vinculantes para eliminar la necesidad de construir dinámicamente una consulta basada en el número de argumentos. Las matrices de objetos anidados se amplían para permitir el uso de (por ejemplo) listas desplegables.

Considere la siguiente consulta:

SELECCIONE id, nombre, estado DE la tabla DONDE (nombre, edad) IN (('John', 35), (' Ann', 50)) 

La consulta anterior se puede parametrizar y ejecutar de la siguiente manera:

Java

List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann",  50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples);
Kotlin

val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples)
Capacidad de uso Las listas desplegables dependen del fabricante.

El siguiente ejemplo muestra una opción más sencilla utilizando predicados IN:

Java

client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("ages", Arrays.asList(35, 50));
Kotlin

val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("tuples", arrayOf(35, 50))
R2DBC en sí no admite valores de tipo Colección. Sin embargo, extender la List dada en el ejemplo anterior funciona para parámetros con nombre en el soporte Spring para R2DBC, como para su uso en expresiones IN como se muestra arriba. Sin embargo, insertar o actualizar columnas con un tipo de matriz (como en Postgres) requiere un tipo de matriz que sea compatible con el controlador R2DBC subyacente: normalmente una matriz Java, como String[] para actualizar un texto[column]. No pase Collection<String> o similares. como parámetro de matriz.

Filtros de declaraciones

A veces es necesario ajustar las opciones de la Statement antes de ejecutarla. Registre un filtro Statement (StatementFilterFunction) a través de DatabaseClient para interceptar y modificar declaraciones a medida que se ejecutan, como se muestra en el siguiente ejemplo:

Java

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
    .bind("name", …)
    .bind("state", …);
Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
            .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
            .bind("name", …)
            .bind("state", …)

DatabaseClient también abre una sobrecarga de filter(...) simplificada que acepta una Función<Statement, Statement>:

Java

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
    .filter(statement -> s.fetchSize(25));
Kotlin

client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
client.sql("SELECT id, name, state FROM table")
    .filter { statement -> s.fetchSize(25) }

Las implementaciones de StatementFilterFunction le permiten filtrar Declaración así como filtrar objetos de Result.

Mejores prácticas para trabajar con DatabaseClient

Una vez configuradas, las instancias de la clase DatabaseClient son seguras para subprocesos. Esto es importante porque significa que puede configurar una instancia de DatabaseClient y luego inyectar de forma segura esa referencia compartida en múltiples DAO (o repositorios). DatabaseClient tiene estado porque almacena una referencia a ConnectionFactory, pero este estado no es un estado conversacional.

Es una práctica común utilizar el class DatabaseClient es configurar un ConnectionFactory en el archivo de configuración de Spring y luego inyectar dependencias con ese bean ConnectionFactory común en las clases DAO. DatabaseClient se crea en el configurador de ConnectionFactory. Esto da como resultado un DAO que se parece a esto:

Java

public class R2dbcCorporateEventDao implements CorporateEventDao {
    private DatabaseClient databaseClient;
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory);
    }
    // Las implementaciones de métodos para CorporateEventDao con soporte R2DBC siguen...
}        
Kotlin

class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {
    private val databaseClient = DatabaseClient.create(connectionFactory)
    // Las implementaciones de métodos para CorporateEventDao con soporte R2DBC siguen...
}

Alternativa La configuración explícita es utilizar escaneo de componentes y anotaciones de soporte para la inyección de dependencia. En este caso, puede marcar la clase con la anotación @Component (lo que la convierte en candidata para el escaneo de componentes) y anotar el configurador ConnectionFactory con @Autowired. El siguiente ejemplo muestra cómo hacer esto:

Java

@Component 
public class R2dbcCorporateEventDao implements CorporateEventDao {
    private DatabaseClient databaseClient;
    @Autowired
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory); 
    }
    // Las implementaciones de métodos para CorporateEventDao con soporte R2DBC siguen...
}
  1. Anota la clase con @Component.
  2. Anota el configurador ConnectionFactory con @Autowired.
  3. Cree un nuevo DatabaseClient usando ConnectionFactory.
Kotlin

@Component 
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { 
    private val databaseClient = DatabaseClient(connectionFactory) 
    // Implementaciones de métodos para CorporateEventDao con soporte R2DBC sigue...
}
  1. Anota la clase usando @Component.
  2. Inyección del constructor de ConnectionFactory.
  3. Crea un nuevo DatabaseClient usando ConnectionFactory .

Independientemente de cuál de los estilos de inicialización de plantilla descritos anteriormente decida usar (o no usar), rara vez es necesario crear una nueva instancia de DatabaseClient clase cada vez que necesite ejecutar SQL. Una vez configurada, la instancia DatabaseClient es segura para subprocesos. Si su aplicación accede a varias bases de datos, es posible que necesite varias instancias de DatabaseClient, lo que requiere varias ConnectionFactory y, por lo tanto, varias instancias de DatabaseClient configuradas de forma diferente.

Obtener claves generadas automáticamente

INSERT puede generar claves al insertar filas en una tabla que identifican una columna de ID o de incremento automático. Para obtener control total sobre el nombre de la columna generada, simplemente registre una StatementFilterFunction que consultará la clave generada para la columna deseada.

Java

Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"))
            .map(row -> row. get("id", Integer.class))
            .first();
// generateId genera la clave generada después de que se completa la instrucción INSERT
Kotlin

val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
        .map { row -> row.get("id", Integer.class) }
        .awaitOne()
// generateId genera la clave generada después de que se completa la instrucción INSERT