App in Java

This page contains a detailed description of the code of a test app that is available as part of the YDB Java SDK.

Downloading SDK Examples and running an example

The startup script below uses Git and Maven.

Create a working directory and use it to run from the command line the command to clone the GitHub repository:

  1. git clone https://github.com/yandex-cloud/ydb-java-sdk

Java - 图1

Next, build the SDK Examples

  1. ( cd ydb-java-sdk/examples && mvn package )

Java - 图2

Next, from the same working directory, run the command to start the test app. The command will differ depending on the database to connect to.

Local Docker

Any database

To connect to a locally deployed YDB database according to the Docker use case, run the following command in the default configuration:

  1. (cd ydb-java-sdk/examples/basic_example/target && \
  2. YDB_ANONYMOUS_CREDENTIALS=1 java -jar ydb-basic-example.jar grpc://localhost:2136?database=/local )

Java - 图3

To run the example using any available YDB database, you need to know the Endpoint and Database location.

If authentication is enabled in the database, you also need to choose the authentication mode and obtain secrets: a token or username/password.

Run the command as follows:

  1. ( cd ydb-java-sdk/examples/basic_example/target && \
  2. <auth_mode_var>="<auth_mode_value>" java -jar ydb-basic-example.jar <endpoint>?database=<database>)

Java - 图4

where

  • <endpoint> is the Endpoint
  • <database> is the DB location.
  • <auth_mode_var> is the Environment variable that determines the authentication mode.
  • <auth_mode_value> is the authentication parameter value for the selected mode.

For example:

  1. YDB_ACCESS_TOKEN_CREDENTIALS="t1.9euelZqOnJuJlc..." java -jar examples/basic_example/target/ydb-basic-example.jar grpcs://ydb.example.com:2135?database=/somepath/somelocation

Java - 图5

Note

If you previously reviewed the articles of the “Getting started” section, you must have used the necessary parameters when getting started with the YDB CLI and can get them from the profile:

  1. ydb config profile get db1

Java - 图6

Initializing a database connection

To interact with YDB, create an instance of the driver, client, and session:

  • The YDB driver lets the app and YDB interact at the transport layer. The driver must exist throughout the YDB access lifecycle and be initialized before creating a client or session.
  • The YDB client runs on top of the YDB driver and enables the handling of entities and transactions.
  • The YDB session contains information about executed transactions and prepared queries, and is part of the YDB client context.

Basic driver initialization parameters:

App code snippet for driver initialization:

  1. GrpcTransport transport = GrpcTransport.forConnectionString(connectionString)
  2. .withAuthProvider(CloudAuthHelper.getAuthProviderFromEnviron())
  3. .build();
  4. GrpcTableRpc rpc = GrpcTableRpc.ownTransport(transport);
  5. this.tableClient = TableClient.newClient(rpc).build();

Java - 图7

We recommend performing all YDB operations using the SessionRetryContext helper class that ensures a correct operation retry in the event of partial unavailability. Code snippet for initializing the retry context:

  1. this.retryCtx = SessionRetryContext.create(tableClient).build();

Java - 图8

Creating tables

Creating tables to be used in operations on a test app. This step results in the creation of DB tables of the series directory data model:

  • Series
  • Seasons
  • Episodes

Once the tables are created, the method for getting information about data schema objects is called and the result of its execution is output.

To create tables, use the Session.CreateTable() method:

  1. private void createTables() {
  2. TableDescription seriesTable = TableDescription.newBuilder()
  3. .addNullableColumn("series_id", PrimitiveType.uint64())
  4. .addNullableColumn("title", PrimitiveType.utf8())
  5. .addNullableColumn("series_info", PrimitiveType.utf8())
  6. .addNullableColumn("release_date", PrimitiveType.date())
  7. .setPrimaryKey("series_id")
  8. .build();
  9. retryCtx.supplyStatus(session -> session.createTable(database + "/series", seriesTable))
  10. .join().expect("create table problem");
  11. TableDescription seasonsTable = TableDescription.newBuilder()
  12. .addNullableColumn("series_id", PrimitiveType.uint64())
  13. .addNullableColumn("season_id", PrimitiveType.uint64())
  14. .addNullableColumn("title", PrimitiveType.utf8())
  15. .addNullableColumn("first_aired", PrimitiveType.date())
  16. .addNullableColumn("last_aired", PrimitiveType.date())
  17. .setPrimaryKeys("series_id", "season_id")
  18. .build();
  19. retryCtx.supplyStatus(session -> session.createTable(database + "/seasons", seasonsTable))
  20. .join().expect("create table problem");
  21. TableDescription episodesTable = TableDescription.newBuilder()
  22. .addNullableColumn("series_id", PrimitiveType.uint64())
  23. .addNullableColumn("season_id", PrimitiveType.uint64())
  24. .addNullableColumn("episode_id", PrimitiveType.uint64())
  25. .addNullableColumn("title", PrimitiveType.utf8())
  26. .addNullableColumn("air_date", PrimitiveType.date())
  27. .setPrimaryKeys("series_id", "season_id", "episode_id")
  28. .build();
  29. retryCtx.supplyStatus(session -> session.createTable(database + "/episodes", episodesTable))
  30. .join().expect("create table problem");
  31. }

Java - 图9

You can use the Session.DescribeTable() method to output information about the table structure and make sure that it was properly created:

  1. private void describeTables() {
  2. logger.info("--[ DescribeTables ]--");
  3. Arrays.asList("series", "seasons", "episodes").forEach(tableName -> {
  4. String tablePath = database + '/' + tableName;
  5. TableDescription tableDesc = retryCtx.supplyResult(session -> session.describeTable(tablePath))
  6. .join().expect("describe table problem");
  7. List<String> primaryKeys = tableDesc.getPrimaryKeys();
  8. logger.info(" table {}", tableName);
  9. for (TableColumn column : tableDesc.getColumns()) {
  10. boolean isPrimary = primaryKeys.contains(column.getName());
  11. logger.info(" {}: {} {}", column.getName(), column.getType(), isPrimary ? " (PK)" : "");
  12. }
  13. });
  14. }

Java - 图10

Adding data

Adding data to the created tables using an UPSERT statement of YQL. A data update request is sent within a single request to the server with transaction auto-commit mode enabled.

Code snippet for inserting and updating data:

  1. private void upsertSimple() {
  2. String query
  3. = "UPSERT INTO episodes (series_id, season_id, episode_id, title) "
  4. + "VALUES (2, 6, 1, \"TBD\");";
  5. // Begin new transaction with SerializableRW mode
  6. TxControl txControl = TxControl.serializableRw().setCommitTx(true);
  7. // Executes data query with specified transaction control settings.
  8. retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl))
  9. .join().expect("execute data query problem");
  10. }

Java - 图11

Retrieving data with a Select

Retrieving data using a SELECT statement in YQL. Handling the retrieved data selection in the app.

To execute YQL queries, use the Session.executeDataQuery() method.
The SDK lets you explicitly control the execution of transactions and configure the transaction execution mode using the TxControl class.

In the code snippet below, the transaction is executed using the session.executeDataQuery() method. The TxControl txControl = TxControl.serializableRw().setCommitTx(true); transaction execution mode and setCommitTx(true) transaction auto complete flag are set. The query body is described using YQL syntax and is passed to the executeDataQuery method as a parameter.

  1. private void selectSimple() {
  2. String query
  3. = "SELECT series_id, title, release_date "
  4. + "FROM series WHERE series_id = 1;";
  5. // Begin new transaction with SerializableRW mode
  6. TxControl txControl = TxControl.serializableRw().setCommitTx(true);
  7. // Executes data query with specified transaction control settings.
  8. DataQueryResult result = retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl))
  9. .join().expect("execute data query");
  10. logger.info("--[ SelectSimple ]--");
  11. ResultSetReader rs = result.getResultSet(0);
  12. while (rs.next()) {
  13. logger.info("read series with id {}, title {} and release_date {}",
  14. rs.getColumn("series_id").getUint64(),
  15. rs.getColumn("title").getUtf8(),
  16. rs.getColumn("release_date").getDate()
  17. );
  18. }
  19. }

Java - 图12

As a result of executing the query, an object of the DataQueryResult class is generated. It may contain several sets obtained using the getResultSet( <index> ) method. Since there was only one SELECT statement in the query, the result contains only one selection indexed as 0. The given code snippet outputs the following text to the console at startup:

  1. 12:06:36.548 INFO App - --[ SelectSimple ]--
  2. 12:06:36.559 INFO App - read series with id 1, title IT Crowd and release_date 2006-02-03

Java - 图13

Parameterized queries

Querying data using parameters. This query execution option is preferable as it allows the server to reuse the query execution plan for subsequent calls and also protects from such vulnerabilities as SQL Injection.

The code snippet below shows the use of parameterized queries and the Params class to generate parameters and pass them to the executeDataQuery method.

  1. private void selectWithParams(long seriesID, long seasonID) {
  2. String query
  3. = "DECLARE $seriesId AS Uint64; "
  4. + "DECLARE $seasonId AS Uint64; "
  5. + "SELECT sa.title AS season_title, sr.title AS series_title "
  6. + "FROM seasons AS sa INNER JOIN series AS sr ON sa.series_id = sr.series_id "
  7. + "WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId";
  8. // Begin new transaction with SerializableRW mode
  9. TxControl txControl = TxControl.serializableRw().setCommitTx(true);
  10. // Type of parameter values should be exactly the same as in DECLARE statements.
  11. Params params = Params.of(
  12. "$seriesId", PrimitiveValue.uint64(seriesID),
  13. "$seasonId", PrimitiveValue.uint64(seasonID)
  14. );
  15. DataQueryResult result = retryCtx.supplyResult(session -> session.executeDataQuery(query, txControl, params))
  16. .join().expect("execute data query");
  17. logger.info("--[ SelectWithParams ] -- ");
  18. ResultSetReader rs = result.getResultSet(0);
  19. while (rs.next()) {
  20. logger.info("read season with title {} for series {}",
  21. rs.getColumn("season_title").getUtf8(),
  22. rs.getColumn("series_title").getUtf8()
  23. );
  24. }
  25. }

Java - 图14

Scan queries

Making a scan query that results in a data stream. Streaming lets you read an unlimited number of rows and amount of data.

  1. private void scanQueryWithParams(long seriesID, long seasonID) {
  2. String query
  3. = "DECLARE $seriesId AS Uint64; "
  4. + "DECLARE $seasonId AS Uint64; "
  5. + "SELECT ep.title AS episode_title, sa.title AS season_title, sr.title AS series_title "
  6. + "FROM episodes AS ep "
  7. + "JOIN seasons AS sa ON sa.season_id = ep.season_id "
  8. + "JOIN series AS sr ON sr.series_id = sa.series_id "
  9. + "WHERE sa.series_id = $seriesId AND sa.season_id = $seasonId;";
  10. // Type of parameter values should be exactly the same as in DECLARE statements.
  11. Params params = Params.of(
  12. "$seriesId", PrimitiveValue.uint64(seriesID),
  13. "$seasonId", PrimitiveValue.uint64(seasonID)
  14. );
  15. logger.info("--[ ExecuteScanQueryWithParams ]--");
  16. retryCtx.supplyStatus(session -> {
  17. ExecuteScanQuerySettings settings = ExecuteScanQuerySettings.newBuilder().build();
  18. return session.executeScanQuery(query, params, settings, rs -> {
  19. while (rs.next()) {
  20. logger.info("read episode {} of {} for {}",
  21. rs.getColumn("episode_title").getUtf8(),
  22. rs.getColumn("season_title").getUtf8(),
  23. rs.getColumn("series_title").getUtf8()
  24. );
  25. }
  26. });
  27. }).join().expect("scan query problem");
  28. }

Java - 图15

Multistep transactions

Multiple commands are executed within a single multistep transaction. The client-side code can be run between query executions. Using a transaction ensures that select queries made in its context are consistent with each other.

To ensure that the retry context works properly while executing transactions, perform each transaction entirely inside the callback passed to SessionRetryContext. The callback should return a response after the transaction is fully completed.

Code template for using complex transactions in SessionRetryContext

  1. private void multiStepTransaction(long seriesID, long seasonID) {
  2. retryCtx.supplyStatus(session -> {
  3. // Multiple operations with session
  4. ...
  5. // return success status to SessionRetryContext
  6. return CompletableFuture.completedFuture(Status.SUCCESS);
  7. }).join().expect("multistep transaction problem");
  8. }

Java - 图16

The first step is to prepare and execute the first query:

  1. String query1
  2. = "DECLARE $seriesId AS Uint64; "
  3. + "DECLARE $seasonId AS Uint64; "
  4. + "SELECT MIN(first_aired) AS from_date FROM seasons "
  5. + "WHERE series_id = $seriesId AND season_id = $seasonId;";
  6. // Execute first query to get the required values to the client.
  7. // Transaction control settings don't set CommitTx flag to keep transaction active
  8. // after query execution.
  9. TxControl tx1 = TxControl.serializableRw().setCommitTx(false);
  10. DataQueryResult res1 = session.executeDataQuery(query1, tx1, Params.of(
  11. "$seriesId", PrimitiveValue.uint64(seriesID),
  12. "$seasonId", PrimitiveValue.uint64(seasonID)
  13. )).join().expect("execute data query problem");

Java - 图17

Next, we can perform some client processing of the data received:

  1. // Perform some client logic on returned values
  2. ResultSetReader resultSet = res1.getResultSet(0);
  3. if (!resultSet.next()) {
  4. throw new RuntimeException("not found first_aired");
  5. }
  6. LocalDate fromDate = resultSet.getColumn("from_date").getDate();
  7. LocalDate toDate = fromDate.plusDays(15);

Java - 图18

And get the current transaction id for further work within a single transaction:

  1. // Get active transaction id
  2. String txId = res1.getTxId();

Java - 图19

The next step is to create the next query that uses the results of code execution on the client side:

  1. // Construct next query based on the results of client logic
  2. String query2
  3. = "DECLARE $seriesId AS Uint64;"
  4. + "DECLARE $fromDate AS Date;"
  5. + "DECLARE $toDate AS Date;"
  6. + "SELECT season_id, episode_id, title, air_date FROM episodes "
  7. + "WHERE series_id = $seriesId AND air_date >= $fromDate AND air_date <= $toDate;";
  8. // Execute second query.
  9. // Transaction control settings continues active transaction (tx) and
  10. // commits it at the end of second query execution.
  11. TxControl tx2 = TxControl.id(txId).setCommitTx(true);
  12. DataQueryResult res2 = session.executeDataQuery(query2, tx2, Params.of(
  13. "$seriesId", PrimitiveValue.uint64(seriesID),
  14. "$fromDate", PrimitiveValue.date(fromDate),
  15. "$toDate", PrimitiveValue.date(toDate)
  16. )).join().expect("execute data query problem");
  17. logger.info("--[ MultiStep ]--");
  18. ResultSetReader rs = res2.getResultSet(0);
  19. while (rs.next()) {
  20. logger.info("read episode {} with air date {}",
  21. rs.getColumn("title").getUtf8(),
  22. rs.getColumn("air_date").getDate()
  23. );
  24. }

Java - 图20

The given code snippets output the following text to the console at startup:

  1. 12:06:36.850 INFO App - --[ MultiStep ]--
  2. 12:06:36.851 INFO App - read episode Grow Fast or Die Slow with air date 2018-03-25
  3. 12:06:36.851 INFO App - read episode Reorientation with air date 2018-04-01
  4. 12:06:36.851 INFO App - read episode Chief Operating Officer with air date 2018-04-08

Java - 图21

Managing transactions

Transactions are managed through TCL Begin and Commit calls.

In most cases, instead of explicitly using Begin and Commit calls, it’s better to use transaction control parameters in execute calls. This helps you avoid unnecessary requests to YDB and run your queries more efficiently.

Code snippet for beginTransaction() and transaction.Commit() calls:

  1. private void tclTransaction() {
  2. retryCtx.supplyStatus(session -> {
  3. Transaction transaction = session.beginTransaction(TransactionMode.SERIALIZABLE_READ_WRITE)
  4. .join().expect("begin transaction problem");
  5. String query
  6. = "DECLARE $airDate AS Date; "
  7. + "UPDATE episodes SET air_date = $airDate WHERE title = \"TBD\";";
  8. Params params = Params.of("$airDate", PrimitiveValue.date(Instant.now()));
  9. // Execute data query.
  10. // Transaction control settings continues active transaction (tx)
  11. TxControl txControl = TxControl.id(transaction).setCommitTx(false);
  12. DataQueryResult result = session.executeDataQuery(query, txControl, params)
  13. .join().expect("execute date query problem");
  14. logger.info("get transaction {}", result.getTxId());
  15. // Commit active transaction (tx)
  16. return transaction.commit();
  17. }).join().expect("tcl transaction problem");
  18. }

Java - 图22