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.function
interfaces 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:
Cliente DatabaseClient = DatabaseClient.create(connectionFactory);
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 unBindMarkersFactory
específico para configure un parámetro con nombre para convertir tokens de enlace de base de datos.….executeFunction(…)
: establezcaExecuteFunction
según qué objetosStatement
se ejecutarán.....namedParameters(false)
: deshabilita la expansión de parámetros con nombre. Habilitado de forma predeterminada.
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:
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
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:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
val first = client.sql("SELECT id, name FROM person")
.fetch().awaitSingle()
A continuación, la solicitud utiliza una variable vinculante:
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
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 llamaawaitSingle()
para valores de retorno que no aceptan valores NULL, yawaitSingleOrNull()
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, concretamenteawaitOne()
para obtener exactamente un valor oawaitOneOrNull()
si el valor puede sernull
.all()
devuelve todas las filas del resultado. Cuando use corrutinas en Kotlin, useflow()
.-
rowsUpdated()
devuelve el número de filas afectadas (contadorINSERT
/UPDATE
/DELETE
). Su variante de rutina de Kotlin se llamaawaitRowsUpdated()
.
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:
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
val names = client.sql("SELECT name FROM person")
.map{ row: Row -> row.get("name", String.class) }
.flow()
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:
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated()
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);
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:
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);
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)
El siguiente ejemplo muestra una opción más sencilla utilizando predicados IN
:
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
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))
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:
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
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>
:
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));
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:
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...
}
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:
@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...
}
- Anota la clase con
@Component
. - Anota el configurador
ConnectionFactory
con@Autowired
. - Cree un nuevo
DatabaseClient
usandoConnectionFactory
.
@Component
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {
private val databaseClient = DatabaseClient(connectionFactory)
// Implementaciones de métodos para CorporateEventDao con soporte R2DBC sigue...
}
- Anota la clase usando
@Component
. - Inyección del constructor de
ConnectionFactory
. - Crea un nuevo
DatabaseClient
usandoConnectionFactory
.
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.
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
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
GO TO FULL VERSION