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