DatabaseClient
— це центральний клас в основному пакеті R2DBC. Він обробляє створення та звільнення
ресурсів, що допомагає уникнути поширених помилок, наприклад, якщо забути закрити з'єднання. Він виконує базові
завдання основного робочого процесу R2DBC (такі як створення та виконання стейтментів), залишаючи код додатка, щоб
надавати SQL та отримувати результати. Клас DatabaseClient
:
Виконує SQL-запити
Оновлює інструкції та виклики процедури, що зберігається
Виконує обхід екземплярів
Result
Перехоплює винятки R2DBC і перетворює їх на типізовану, більш інформативну ієрархію винятків, визначену у пакеті
org.springframework.dao
. (див. Узгоджена ієрархія винятків)
Клієнт має функціональний, вільний API-інтерфейс, який використовує реактивні типи для декларативної компонування.
Якщо ви використовуєте DatabaseClient
для свого коду, то необхідно лише
реалізувати інтерфейси java.util.function
, надавши їм чітко визначений контракт. Враховуючи
Connection
, наданий класом DatabaseClient
, зворотний виклик Function
створює
Publisher
. Те саме стосується і функцій відображення, які отримують результат Row
.
Можна використовувати DatabaseClient
всередині реалізації DAO шляхом прямого створення екземпляра з
посиланням на ConnectionFactory
, або ж можна конфігурувати його в IoC-контейнері Spring і передати
його DAO-об'єктам як посилання на бін.
Найпростіший спосіб створення об'єкта DatabaseClient
— це
статичний фабричний метод, як показано нижче:
DatabaseClient client = DatabaseClient.create(connectionFactory);
val client = DatabaseClient.create(connectionFactory)
ConnectionFactory
завжди потрібно конфігурувати в
IoC-контейнері Spring як бін.
Попередній метод створює DatabaseClient
із налаштуваннями за замовчуванням.
Також можна
отримати екземпляр Builder
з DatabaseClient.builder()
. Можна налаштувати клієнт,
якщо викликати такі методи:
….bindMarkers(…)
: вкажіть конкретну фабрикуBindMarkersFactory
, щоб налаштувати іменований параметр для перетворення маркерів прив'язки бази даних.….executeFunction(…)
: встановітьExecuteFunction
, відповідно до якої будуть виконуватися об'єктиStatement
.….namedParameters(false)
: вимкніть розширення іменованих параметрів. Увімкнено за замовчуванням.
BindMarkersFactoryResolver
з ConnectionFactory
, зазвичай шляхом
перевірки ConnectionFactoryMetadata
. Ти можеш дозволити Spring автоматично виявляти свою BindMarkersFactory
,
зареєструвавши клас, що реалізує org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider
через META-INF factories
. BindMarkersFactoryResolver
виявляє реалізації постачальників
маркерів прив'язки з classpath за допомогою використання SpringFactoriesLoader
.
Наразі підтримуються такі бази даних:
H2
MariaDB
Microsoft SQL Server
MySQL
Postgres
Всі SQL-запити, що видаються цим класом, реєструються на рівні DEBUG
під категорією, що
відповідає повному імені класу екземпляра клієнта (зазвичай DefaultDatabaseClient
). До того ж, кожне
виконання реєструє контрольну точку реактивної послідовності для полегшення налагодження.
У наступних розділах наведено деякі приклади використання DatabaseClient
. Ці приклади не є вичерпним
списком всіх функціональних можливостей, що надаються DatabaseClient
. Див. про це у супутньому javadoc.
Виконання стейтментів
DatabaseClient
забезпечує базову функціональність виконання стейтмента. У цьому прикладі показано,
що має бути у складі мінімального, але повністю функціонального коду, який створює нову таблицю:
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
розроблений для зручного, вільного використання. Він розкриває проміжні методи, методи,
що продовжують
та кінцеві методи на кожному етапі специфікації виконання. У попередньому прикладі використовується
then()
для повернення Publisher
, який завершується, щойно запит (або
запити, якщо SQL-запит містить кілька стейтментів) завершиться.
execute(…)
приймає або рядок SQL-запиту, або запит Supplier<String>
,
щоб відкласти фактичне створення запиту до його виконання.
Побудова запитів (SELECT
)
SQL-запити можуть повертати значення через об'єкти Row
або кількість рядків, яких це стосуєтья. DatabaseClient
може повернути кількість оновлених рядків або самі рядки, залежно від виданого запиту.
Наступний запит отримує стовпці id
та name
з таблиці:
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()
У наступному запиті використовується змінна зв'язування:
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()
Можна помітити використання fetch()
у прикладі вище. fetch()
— це продовжувальний
оператор, який дозволяє зазначити, скільки даних потрібно спожити.
Виклик first()
повертає перший рядок з результату і відкидає залишок рядків. Дані можна
використовувати за допомогою наступних операторів:
first()
повертає перший рядок всього результату. Його різновид співпрограми на Kotlin називаєтьсяawaitSingle()
для повертаємих значень, що не допускають порожнього значення, іawaitSingleOrNull(),
якщо значення необов'язкове.one()
повертає рівно один результат і завершується помилкою, якщо результат містить більше рядків. Використовуються співпрограми Kotlin, а самеawaitOne()
для отримання рівно одного значення абоawaitOneOrNull()
, якщо значення може бутиnull
.all()
повертає всі рядки результату. При використанні співпрограм на Kotlin застосовуйflow()
.rowsUpdated()
повертає кількість зачеплених рядків (лічильникINSERT
/UPDATE
/DELETE
). Його варіант співпрограми на Kotlin називаєтьсяawaitRowsUpdated()
.
Без зазначення подальших деталей відображення запити повертають табличні результати у вигляді Map
,
ключами яких є імена стовпців без урахування регістру, які відображаються в значення стовпців.
Можна управляти поданням результатів, якщо надати Function<Row, T>
, щоб
вона могла повертати довільні значення (поодинокі значення, колекції, Map і об'єкти).
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()
Оновлення (INSERT
, UPDATE
та DELETE
) за допомогою
DatabaseClient
Єдина відмінність модифікуючих інструкцій полягає в тому, що ці інструкції зазвичай не повертають табличні
дані, тому для отримання результатів використовується функція rowsUpdated()
.
У наступному прикладі показано інструкцію UPDATE
, яка повертає кількість оновлених рядків:
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()
Прив'язка значень до запитів
Типова програма вимагає параметризованих SQL-інструкцій для вибірки або оновлення рядків відповідно до певних
вхідних даними. Зазвичай це інструкції SELECT
, обмежені виразом WHERE
, або інструкції
INSERT
та UPDATE
, які приймають вхідні параметри. Параметризовані інструкції несуть
ризик застосування SQL, якщо параметри не приведені до належного вигляду. DatabaseClient
використовує
API-інтерфейс bind
з R2DBC для усунення ризику впровадження SQL для параметрів запиту. Можна
вказати параметризовану SQL-інструкцію за допомогою оператора execute(…)
та прив'язати параметри до
фактичної Statement
. Потім драйвер R2DBC запускає стейтмент за допомогою скомпільованого стейтмента
та підстановки параметрів.
Прив'язка параметрів підтримує дві стратегії прив'язки:
За індексом, використовуючи нульові індекси параметрів.
На ім'я, використовуючи ім'я-плейсхолдер.
У наступному прикладі показано прив'язування параметрів для запиту:
db.sql("INSERT INTO person (id, name , age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
Препроцесор запитів розгортає іменовані параметри Collection
у серію маркерів прив'язки, щоб усунути
необхідність динамічного створення запиту залежно від кількості аргументів. Вкладені масиви об'єктів
розширюються, дозволяючи використовувати (наприклад) списки, що розкриваються.
Розглянемо наступний запит:
SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))
Попередній запит можна налаштувати та виконати таким чином:
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)
У наступному прикладі показаний простіший варіант з використанням предикатів 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
у наведеному вище прикладі працює для іменованих параметрів у засобах підтримки Spring
для R2DBC, наприклад, для використання у виразах IN
, як показано вище. Однак для вставки або
оновлення стовпців з типом масиву (наприклад, Postgres) потрібен тип масиву, який підтримується базовим
драйвером R2DBC: зазвичай це масив Java, наприклад, String[]
для оновлення стовпця text[]
.
Не передавай Collection<String>
як параметр масиву.
Фільтри стейтментів
Іноді потрібно тонко налаштувати опції для самої Statement
, перш ніж вона запуститься.
Зареєструй фільтр Statement
(StatementFilterFunction
) через
DatabaseClient
для перехоплення та модифікації інструкцій у процесі їх виконання, як показано в
наступному прикладі:
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
відкриває також спрощене навантаження filter(…)
, яке приймає Function<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) }
Реалізації StatementFilterFunction дозволяють фільтрувати Statement
, а також фільтрувати об'єкти
Result
.
Оптимальні методи роботи з DatabaseClient
Після конфігурування екземпляри класу DatabaseClient
є потокобезпечними. Це важливо, оскільки
означає, що можна налаштувати один екземпляр DatabaseClient
, а потім безпечно впроваджувати це
загальне посилання до кількох DAO (або репозиторіїв). DatabaseClient
зберігає стан, оскільки зберігає
посилання на ConnectionFactory
, але цей стан не є діалоговим станом.
Загальноприйнятим методом роботи під час використання класу DatabaseClient
є конфігурація ConnectionFactory
у конфігураційному файлі Spring, а потім впровадження залежностей із цим загальним біном
ConnectionFactory
до класів DAO. DatabaseClient
створюється в сетері для ConnectionFactory
.
Це призводить до появи DAO приблизно такого вигляду:
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// Реалізації методів для CorporateEventDao за допомогою R2DBC слідують...
}
class R2dbcCorporateEventDao(connectionFactory : ConnectionFactory) : CorporateEventDao {
private val databaseClient = DatabaseClient.create(connectionFactory)
// Реалізації методів для CorporateEventDao з підтримкою R2DBC слідують...
}
Альтернативою явній конфігурації є використання сканування компонентів та підтримка інструкцій для застосування
залежностей. У цьому випадку можна позначити клас за допомогою анотації @Component
(що робить його
кандидатом на сканування компонентів) та анотувати метод встановлення ConnectionFactory
за
допомогою @Autowired
. У цьому прикладі показано, як це зробити:
@Component
public class R2dbcCorporateEventDao implements CorporateEventDao {
private DatabaseClient databaseClient;
@Autowired
public void setConnectionFactory(ConnectionFactory connectionFactory) {
this.databaseClient = DatabaseClient.create(connectionFactory);
}
// Реалізації методів для CorporateEventDao за допомогою R2DBC слідують...
}
- Анотуємо клас за допомогою
@Component
. - Анотуємо сетер
ConnectionFactory
за допомогою@Autowired
. - Створюємо новий
DatabaseClient
за допомогоюConnectionFactory
.
@Component
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {
private val databaseClient = DatabaseClient(connectionFactory)
// Реалізації методів для CorporateEventDao з підтримкою R2DBC слідують...
}
- Анотуємо клас за допомогою
@Component
. - Constructor injection of the
ConnectionFactory
. - Створюємо новий
DatabaseClient
за допомогоюConnectionFactory
.
Незалежно від того, який із описаних вище стилів ініціалізації шаблону ти вирішиш використовувати (або не
використовувати), рідко виникає необхідність створювати новий екземпляр класу DatabaseClient
щоразу, коли необхідно виконати SQL. Після конфігурування екземпляр DatabaseClient
є безпечним.
Якщо твій додаток звертається до кількох баз даних, може знадобитися кілька екземплярів
DatabaseClient
, що потребує наявності кількох ConnectionFactory
і, відповідно, кількох
по-різному налаштованих екземплярів DatabaseClient
.
Отримання автоматично згенерованих ключів
Інструкції INSERT
можуть генерувати ключі під час вставки рядків до таблиці, які визначають стовпець з
автоінкрементом або ідентифікатором. Щоб отримати повний контроль над ім'ям стовпця, що генерується, просто
зареєструй
StatementFilterFunction
, яка буде робити запит на згенерований ключ для потрібного стовпця.
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();
// generatedId породжує згенерований ключ після завершення інструкції 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()
// generatedId породжує згенерований ключ після завершення інструкції INSERT
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ