Приложение на Java
На этой странице подробно разбирается код тестового приложения, доступного в составе Java SDK Examples YDB.
Скачивание SDK Examples и запуск примера
Приведенный ниже сценарий запуска использует git и Maven.
Создайте рабочую директорию и выполните в ней из командной строки команду клонирования репозитория с github.com:
git clone https://github.com/ydb-platform/ydb-java-examples
Далее выполните сборку SDK Examples
( cd ydb-java-examples && mvn package )
Далее из этой же рабочей директории выполните команду запуска тестового приложения, которая будет отличаться в зависимости от того, к какой базе данных необходимо подключиться.
Для соединения с развернутой локальной базой данных YDB по сценарию Docker в конфигурации по умолчанию выполните следующую команду:
(cd ydb-java-examples/basic_example/target && \
YDB_ANONYMOUS_CREDENTIALS=1 java -jar ydb-basic-example.jar grpc://localhost:2136?database=/local )
Для выполнения примера с использованием любой доступной базы данных YDB вам потребуется знать эндпоинт и путь базы данных.
Если в базе данных включена аутентификация, то вам также понадобится выбрать режим аутентификации и получить секреты - токен или логин/пароль.
Выполните команду по следующему образцу:
( cd ydb-java-examples/basic_example/target && \
<auth_mode_var>="<auth_mode_value>" java -jar ydb-basic-example.jar <endpoint>?database=<database>)
, где
<endpoint>
- эндпоинт.<database>
- путь базы данных.<auth_mode_var
> - переменная окружения, определяющая режим аутентификации.<auth_mode_value>
- значение параметра аутентификации для выбранного режима.
Например:
( cd ydb-java-examples/basic_example/target && \
YDB_ACCESS_TOKEN_CREDENTIALS="t1.9euelZqOnJuJlc..." java -jar ydb-basic-example.jar grpcs://ydb.example.com:2135?database=/somepath/somelocation)
Инициализация соединения с базой данных
Для взаимодействия с YDB создается экземпляр драйвера, клиента и сессии:
- Драйвер YDB отвечает за взаимодействие приложения и YDB на транспортном уровне. Драйвер должен существовать на всем протяжении жизненного цикла работы с YDB и должен быть инициализирован перед созданием клиента и сессии.
- Клиент YDB работает поверх драйвера YDB и отвечает за работу с сущностями и транзакциями.
- Сессия YDB содержит информацию о выполняемых транзакциях и подготовленных запросах и содержится в контексте клиента YDB.
Основные параметры инициализации драйвера
- Cтрока подключения с информацией об эндпоинте и базе данных. Единственный обязательный параметр.
- Провайдер аутенфикации. В случае отсутствия прямого указания будет использоваться анонимное подключение.
- Настройки пула сессий
Фрагмент кода приложения для инициализации драйвера:
this.transport = GrpcTransport.forConnectionString(connectionString)
.withAuthProvider(CloudAuthHelper.getAuthProviderFromEnviron())
.build();
this.tableClient = TableClient.newClient(transport).build();
Все операции с YDB рекомендуется выполнять с помощью класса-хелпера SessionRetryContext
, который обеспечивает корректное повторное выполнение операции в случае частичной недоступности. Фрагмент кода для инициализации контекста ретраев:
this.retryCtx = SessionRetryContext.create(tableClient).build();
Создание таблиц
Выполняется создание таблиц, которые используются в дальнейших операциях тестового приложения. В результате исполнения шага в базе данных будут созданы таблицы модели данных справочника сериалов:
series
- Сериалыseasons
- Сезоныepisodes
- Эпизоды
После создания вызывается метод получения информации об объекте схемы данных, и выводится результат его выполнения.
Для создания таблиц используется метод Session.createTable()
:
private void createTables() {
TableDescription seriesTable = TableDescription.newBuilder()
.addNullableColumn("series_id", PrimitiveType.Uint64)
.addNullableColumn("title", PrimitiveType.Text)
.addNullableColumn("series_info", PrimitiveType.Text)
.addNullableColumn("release_date", PrimitiveType.Date)
.setPrimaryKey("series_id")
.build();
retryCtx.supplyStatus(session -> session.createTable(database + "/series", seriesTable))
.join().expectSuccess("Can't create table /series");
TableDescription seasonsTable = TableDescription.newBuilder()
.addNullableColumn("series_id", PrimitiveType.Uint64)
.addNullableColumn("season_id", PrimitiveType.Uint64)
.addNullableColumn("title", PrimitiveType.Text)
.addNullableColumn("first_aired", PrimitiveType.Date)
.addNullableColumn("last_aired", PrimitiveType.Date)
.setPrimaryKeys("series_id", "season_id")
.build();
retryCtx.supplyStatus(session -> session.createTable(database + "/seasons", seasonsTable))
.join().expectSuccess("Can't create table /seasons");
TableDescription episodesTable = TableDescription.newBuilder()
.addNullableColumn("series_id", PrimitiveType.Uint64)
.addNullableColumn("season_id", PrimitiveType.Uint64)
.addNullableColumn("episode_id", PrimitiveType.Uint64)
.addNullableColumn("title", PrimitiveType.Text)
.addNullableColumn("air_date", PrimitiveType.Date)
.setPrimaryKeys("series_id", "season_id", "episode_id")
.build();
retryCtx.supplyStatus(session -> session.createTable(database + "/episodes", episodesTable))
.join().expectSuccess("Can't create table /episodes");
}
С помощью метода Session.describeTable()
можно вывести информацию о структуре таблицы и убедиться, что она успешно создалась:
private void describeTables() {
logger.info("--[ DescribeTables ]--");
Arrays.asList("series", "seasons", "episodes").forEach(tableName -> {
String tablePath = database + '/' + tableName;
TableDescription tableDesc = retryCtx.supplyResult(session -> session.describeTable(tablePath))
.join().getValue();
List<String> primaryKeys = tableDesc.getPrimaryKeys();
logger.info(" table {}", tableName);
for (TableColumn column : tableDesc.getColumns()) {
boolean isPrimary = primaryKeys.contains(column.getName());
logger.info(" {}: {} {}", column.getName(), column.getType(), isPrimary ? " (PK)" : "");
}
});
}
Запись данных
Выполняется запись данных в созданные таблицы с использованием команды UPSERT
языка запросов YQL. Применяется режим передачи запроса на изменение данных с автоматическим подтверждением транзакции в одном запросе к серверу.
Фрагмент кода, демонстрирующий выполнение запроса на запись/изменение данных:
private void upsertSimple() {
String query
= "UPSERT INTO episodes (series_id, season_id, episode_id, title) "
+ "VALUES (2, 6, 1, \"TBD\");";
// Begin new transaction with SerializableRW mode
TxControl txControl = TxControl.serializableRw().setCommitTx(true);
// Executes data query with specified transaction control settings.
retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl))
.join().getValue();
}
Получение выборки данных
Выполняется запрос на получение выборки данных с использованием команды SELECT
языка запросов YQL. Демонстрируется обработка полученной выборки в приложении.
Для выполнения YQL-запросов используется метод Session.executeDataQuery()
.
SDK позволяет в явном виде контролировать выполнение транзакций и настраивать необходимый режим выполнения транзакций с помощью класса TxControl
.
В фрагменте кода, приведенного ниже, транзакция выполняется с помощью метода session.executeDataQuery()
. Устанавливается режим выполнения транзакции TxControl txControl = TxControl.serializableRw().setCommitTx(true);
и флаг автоматического завершения транзакции setCommitTx(true)
. Тело запроса описано с помощью синтаксиса YQL и как параметр передается методу executeDataQuery
.
private void selectSimple() {
String query
= "SELECT series_id, title, release_date "
+ "FROM series WHERE series_id = 1;";
// Begin new transaction with SerializableRW mode
TxControl txControl = TxControl.serializableRw().setCommitTx(true);
// Executes data query with specified transaction control settings.
DataQueryResult result = retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl))
.join().getValue();
logger.info("--[ SelectSimple ]--");
ResultSetReader rs = result.getResultSet(0);
while (rs.next()) {
logger.info("read series with id {}, title {} and release_date {}",
rs.getColumn("series_id").getUint64(),
rs.getColumn("title").getText(),
rs.getColumn("release_date").getDate()
);
}
}
В результате исполнения запроса формируется объект класса DataQueryResult
, который может содержать несколько выборок, получаемых методом getResultSet( <index> )
. Так как запрос содержал только одну команду SELECT
, то результат содержит только одну выборку под индексом 0
. Приведенный фрагмент кода при запуске выводит на консоль текст:
12:06:36.548 INFO App - --[ SelectSimple ]--
12:06:36.559 INFO App - read series with id 1, title IT Crowd and release_date 2006-02-03
Параметризованные запросы
Выполняется запрос к данным с использованием параметров. Этот вариант выполнения запросов является предпочтительным, так как позволяет серверу переиспользовать план исполнения запроса при последующих его вызовах, а также спасает от уязвимостей вида SQL Injection.
Фрагмент кода, приведенный ниже, демонстрирует использование параметризованных запросов и класс Params
для формирования параметров и передачи их методу executeDataQuery
.
private void selectWithParams(long seriesID, long seasonID) {
String query
= "DECLARE $seriesId AS Uint64; "
+ "DECLARE $seasonId AS Uint64; "
+ "SELECT sa.title AS season_title, sr.title AS series_title "
+ "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id "
+ "WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId";
// Begin new transaction with SerializableRW mode
TxControl txControl = TxControl.serializableRw().setCommitTx(true);
// Type of parameter values should be exactly the same as in DECLARE statements.
Params params = Params.of(
"$seriesId", PrimitiveValue.newUint64(seriesID),
"$seasonId", PrimitiveValue.newUint64(seasonID)
);
DataQueryResult result = retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl, params))
.join().getValue();
logger.info("--[ SelectWithParams ] -- ");
ResultSetReader rs = result.getResultSet(0);
while (rs.next()) {
logger.info("read season with title {} for series {}",
rs.getColumn("season_title").getText(),
rs.getColumn("series_title").getText()
);
}
}
Скан запросы
Выполняется скан запрос данных, результатом исполнения которого является стрим. Стрим позволяет считать неограниченное количество строк и объем данных.
private void scanQueryWithParams(long seriesID, long seasonID) {
String query
= "DECLARE $seriesId AS Uint64; "
+ "DECLARE $seasonId AS Uint64; "
+ "SELECT ep.title AS episode_title, sa.title AS season_title, sr.title AS series_title "
+ "FROM episodes AS ep "
+ "JOIN seasons AS sa ON sa.season_id = ep.season_id "
+ "JOIN series AS sr ON sr.series_id = sa.series_id "
+ "WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId;";
// Type of parameter values should be exactly the same as in DECLARE statements.
Params params = Params.of(
"$seriesId", PrimitiveValue.newUint64(seriesID),
"$seasonId", PrimitiveValue.newUint64(seasonID)
);
logger.info("--[ ExecuteScanQueryWithParams ]--");
retryCtx.supplyStatus(session -> {
ExecuteScanQuerySettings settings = ExecuteScanQuerySettings.newBuilder().build();
return session.executeScanQuery(query, params, settings, rs -> {
while (rs.next()) {
logger.info("read episode {} of {} for {}",
rs.getColumn("episode_title").getText(),
rs.getColumn("season_title").getText(),
rs.getColumn("series_title").getText()
);
}
});
}).join().expectSuccess("scan query problem");
}
Многошаговые транзакции
Выполняется несколько команд в рамках одной многошаговой транзакции. Между выполнением запросов допустимо выполнение работы кода клиентского приложения. Использование транзакции позволяет гарантировать, что выполненные в её контексте выборки консистентны между собой.
Для обеспечения корректности совместной работы транзакций и контекста ретраев каждая транзакция должна выполняться целиком внутри callback, передаваемого в SessionRetryContext
. Возврат из callback должен происходить после полного завершения транзакции.
Шаблон кода по использованию сложных транзакций в SessionRetryContext
private void multiStepTransaction(long seriesID, long seasonID) {
retryCtx.supplyStatus(session -> {
// Multiple operations with session
...
// return success status to SessionRetryContext
return CompletableFuture.completedFuture(Status.SUCCESS);
}).join().expectSuccess("multistep transaction problem");
}
Первый шаг — подготовка и выполнение первого запроса:
String query1
= "DECLARE $seriesId AS Uint64; "
+ "DECLARE $seasonId AS Uint64; "
+ "SELECT MIN(first_aired) AS from_date FROM seasons "
+ "WHERE series_id = $seriesId AND season_id = $seasonId;";
// Execute first query to get the required values to the client.
// Transaction control settings don't set CommitTx flag to keep transaction active
// after query execution.
TxControl tx1 = TxControl.serializableRw().setCommitTx(false);
DataQueryResult res1 = session.executeDataQuery(query1, tx1, Params.of(
"$seriesId", PrimitiveValue.newUint64(seriesID),
"$seasonId", PrimitiveValue.newUint64(seasonID)
)).join().getValue();
Затем мы можем выполнить некоторую клиентскую обработку полученных данных:
// Perform some client logic on returned values
ResultSetReader resultSet = res1.getResultSet(0);
if (!resultSet.next()) {
throw new RuntimeException("not found first_aired");
}
LocalDate fromDate = resultSet.getColumn("from_date").getDate();
LocalDate toDate = fromDate.plusDays(15);
И получить текущий transaction id
для дальшейшей работы в рамках одной транзакции:
// Get active transaction id
String txId = res1.getTxId();
Следующий шаг — создание следующего запроса, использующего результаты выполнения кода на стороне клиентского приложения:
// Construct next query based on the results of client logic
String query2
= "DECLARE $seriesId AS Uint64;"
+ "DECLARE $fromDate AS Date;"
+ "DECLARE $toDate AS Date;"
+ "SELECT season_id, episode_id, title, air_date FROM episodes "
+ "WHERE series_id = $seriesId AND air_date >= $fromDate AND air_date <= $toDate;";
// Execute second query.
// Transaction control settings continues active transaction (tx) and
// commits it at the end of second query execution.
TxControl tx2 = TxControl.id(txId).setCommitTx(true);
DataQueryResult res2 = session.executeDataQuery(query2, tx2, Params.of(
"$seriesId", PrimitiveValue.newUint64(seriesID),
"$fromDate", PrimitiveValue.newDate(fromDate),
"$toDate", PrimitiveValue.newDate(toDate)
)).join().getValue();
logger.info("--[ MultiStep ]--");
ResultSetReader rs = res2.getResultSet(0);
while (rs.next()) {
logger.info("read episode {} with air date {}",
rs.getColumn("title").getText(),
rs.getColumn("air_date").getDate()
);
}
Приведенные фрагменты кода при запуске выводят на консоль текст:
12:06:36.850 INFO App - --[ MultiStep ]--
12:06:36.851 INFO App - read episode Grow Fast or Die Slow with air date 2018-03-25
12:06:36.851 INFO App - read episode Reorientation with air date 2018-04-01
12:06:36.851 INFO App - read episode Chief Operating Officer with air date 2018-04-08
Управление транзакциями
Выполняются вызовы операторов управления транзакциями TCL - Begin и Commit.
В большинстве случаев вместо явного использования вызовов Begin и Commit лучше использовать параметры контроля транзакций в вызовах execute. Это позволит избежать лишних обращений к YDB и эффективней выполнять запросы.
Фрагмент кода, демонстрирующий явное использование вызовов beginTransaction()
и transaction.commit()
:
private void tclTransaction() {
retryCtx.supplyStatus(session -> {
Transaction transaction = session.beginTransaction(Transaction.Mode.SERIALIZABLE_READ_WRITE)
.join().getValue();
String query
= "DECLARE $airDate AS Date; "
+ "UPDATE episodes SET air_date = $airDate WHERE title = \"TBD\";";
Params params = Params.of("$airDate", PrimitiveValue.newDate(Instant.now()));
// Execute data query.
// Transaction control settings continues active transaction (tx)
TxControl txControl = TxControl.id(transaction).setCommitTx(false);
DataQueryResult result = session.executeDataQuery(query, txControl, params)
.join().getValue();
logger.info("get transaction {}", result.getTxId());
// Commit active transaction (tx)
return transaction.commit();
}).join().expectSuccess("tcl transaction problem");
}