Приложение на Node.js

На этой странице представлено подробное описание кода тестового приложения,
доступого в составе YDB Node.js SDK YDB.

Инициализация соединения с базой данных

Для взаимодействия с YDB создается экземпляр драйвера, клиента и сессии:

  • Драйвер YDB отвечает за взаимодействие приложения и YDB на транспортном уровне. Драйвер должен существовать на всем протяжении жизненного цикла работы с YDB и должен быть инициализирован перед созданием клиента и сессии.
  • Клиент YDB работает поверх драйвера YDB и отвечает за работу с сущностями и транзакциями.
  • Сессия YDB содержит информацию о выполняемых транзакциях и подготовленных запросах и содержится в контексте клиента YDB.

Фрагмент кода приложения для инициализации драйвера:

const authService = getCredentialsFromEnv();
logger.debug('Driver initializing...');
const driver = new Driver({connectionString, authService});
const timeout = 10000;
if (!await driver.ready(timeout)) {
    logger.fatal(`Driver did not become ready within ${timeout}ms!`);
    process.exit(1);
}
const authService = getCredentialsFromEnv();
logger.debug('Driver initializing...');
const driver = new Driver({endpoint, database, authService});
const timeout = 10000;
if (!await driver.ready(timeout)) {
    logger.fatal(`Driver did not become ready within ${timeout}ms!`);
    process.exit(1);
}

Фрагмент кода приложения для создания сессии:

const result = await driver.queryClient.do({
    ...
    fn: async (session) => {
        ...
    }
});

Создание строковых таблиц

Выполняется создание строковых таблиц, которые используются в дальнейших операциях тестового приложения. В результате исполнения шага в базе данных будут созданы строковые таблицы модели данных справочника сериалов:

  • series - Сериалы
  • seasons - Сезоны
  • episodes - Эпизоды

После создания вызывается метод получения информации об объекте схемы данных, и выводится результат его выполнения.

async function createTables(driver: Driver, logger: Logger) {
    logger.info('Dropping old tables and creating new ones...');
    await driver.queryClient.do({
        fn: async (session) => {

          try {
              await session.execute({
                  text: `
                      DROP TABLE ${SERIES_TABLE};
                      DROP TABLE ${EPISODES_TABLE};
                      DROP TABLE ${SEASONS_TABLE};`,
              });
          } catch (err) { // Ignore if tables are missing
              if (err instanceof SchemeError) throw err;
          }

          await session.execute({
              text: `
                  CREATE TABLE ${SERIES_TABLE}
                  (
                      series_id    UInt64,
                      title        Utf8,
                      series_info  Utf8,
                      release_date DATE,
                      PRIMARY KEY (series_id)
                  );

                  CREATE TABLE ${SEASONS_TABLE}
                  (
                      series_id   UInt64,
                      season_id   UInt64,
                      title UTF8,
                      first_aired DATE,
                      last_aired DATE,
                      PRIMARY KEY (series_id, season_id)
                  );

                  CREATE TABLE ${EPISODES_TABLE}
                  (
                      series_id  UInt64,
                      season_id  UInt64,
                      episode_id UInt64,
                      title      UTf8,
                      air_date   DATE,
                      PRIMARY KEY (series_id, season_id, episode_id),
                      INDEX      episodes_index GLOBAL ASYNC ON (air_date)
                  );`,
          });
        },
    });
}

Запись данных

Выполняется запись данных в созданные строковые таблицы с использованием команды UPSERT языка запросов YQL. Применяется режим передачи запроса на изменение данных с автоматическим подтверждением транзакции в одном запросе к серверу.

Фрагмент кода, демонстрирующий выполнение запроса на запись/изменение данных:

async function upsertSimple(driver: Driver, logger: Logger): Promise<void> {
    logger.info('Making an upsert...');
    await driver.queryClient.do({
        fn: async (session) => {
             await session.execute({
                 text: `
                    UPSERT INTO ${EPISODES_TABLE} (series_id, season_id, episode_id, title)
                    VALUES (2, 6, 1, "TBD");`,
           })
        }
    });
    logger.info('Upsert completed.')
}

Получение выборки данных

Выполняется запрос на получение выборки данных с использованием команды SELECT языка запросов YQL. Демонстрируется обработка полученной выборки в приложении.

Для выполнения YQL-запросов используется метод QuerySession.execute().

В зависимости оп параметра rowMode данные можно получить в JavaScript форме или как YDB структуры.

async function selectNativeSimple(driver: Driver, logger: Logger): Promise<void> {
    logger.info('Making a simple native select...');
    const result = await driver.queryClient.do({
        fn: async (session) => {
            const {resultSets} =
                await session.execute({
                    // rowMode: RowType.Native, // Result set columns and rows are returned as native JavaScript values. This is the default behavior.
                    text: `
                        SELECT series_id,
                               title,
                               release_date
                        FROM ${SERIES_TABLE}
                        WHERE series_id = 1;`,
                });
            const {value: resultSet1} = await resultSets.next();
            const rows: any[][] = []
            for await (const row of resultSet1.rows) rows.push(row);
            return {cols: resultSet1.columns, rows};
        }
    });
    logger.info(`selectNativeSimple cols: ${JSON.stringify(result.cols, null, 2)}`);
    logger.info(`selectNativeSimple rows: ${JSON.stringify(result.rows, null, 2)}`);
}
async function selectTypedSimple(driver: Driver, logger: Logger): Promise<void> {
    logger.info('Making a simple typed select...');
    const result = await driver.queryClient.do({
        fn: async (session) => {
            const {resultSets} =
                await session.execute({
                    rowMode: RowType.Ydb, // enables typedRows() on result sets
                    text: `
                        SELECT series_id,
                               title,
                               release_date
                        FROM ${SERIES_TABLE}
                        WHERE series_id = 1;`,
                });
            const {value: resultSet1} = await resultSets.next();
            const rows: Series[] = [];
            // Note: resultSet1.rows will iterate YDB IValue structures
            for await (const row of resultSet1.typedRows(Series)) rows.push(row);
            return {cols: resultSet1.columns, rows};
        }
    });
    logger.info(`selectTypedSimple cols: ${JSON.stringify(result.cols, null, 2)}`);
    logger.info(`selectTypedSimple rows: ${JSON.stringify(result.rows, null, 2)}`);
}

Параметризованные запросы

Выполняется запрос к данным с использованием параметров. Этот вариант выполнения запросов является предпочтительным, так как позволяет серверу переиспользовать план исполнения запроса при последующих его вызовах, а также спасает от уязвимостей вида SQL Injection.

async function selectWithParameters(driver: Driver, data: ThreeIds[], logger: Logger): Promise<void> {

  await driver.queryClient.do({
      fn: async (session) => {
          for (const [seriesId, seasonId, episodeId] of data) {
              const episode = new Episode({seriesId, seasonId, episodeId, title: '', airDate: new Date()});
              const {resultSets, opFinished} = await session.execute({
                  parameters: {
                      '$seriesId': episode.getTypedValue('seriesId'),
                      '$seasonId': episode.getTypedValue('seasonId'),
                      '$episodeId': episode.getTypedValue('episodeId')
                  },
                  text: `
                      DECLARE $seriesId AS Uint64;
                      DECLARE $seasonId AS Uint64;
                      DECLARE $episodeId AS Uint64;

                      SELECT title,
                             air_date
                      FROM episodes
                      WHERE series_id = $seriesId
                        AND season_id = $seasonId
                        AND episode_id = $episodeId;`
              });
              const {value: resultSet} = await resultSets.next();
              const {value: row} = await resultSet.rows.next();
              await opFinished;
              logger.info(`Parametrized select query ${JSON.stringify(row, null, 2)}`);
          }
      }
  });
}

Скан запросы

Выполняется скан запрос данных, результатом исполнения которого является стрим. Стрим позволяет считать неограниченное количество строк и объем данных.

Для получения данных потоком используется метод QuerySession.execute().

async function selectWithParametrs(driver: Driver, data: ThreeIds[], logger: Logger): Promise<void> {
  logger.info('Selecting prepared query...');
  await driver.queryClient.do({
      fn: async (session) => {
          for (const [seriesId, seasonId, episodeId] of data) {

              const episode = new Episode({seriesId, seasonId, episodeId, title: '', airDate: new Date()});

              const {resultSets, opFinished} = await session.execute({
                  parameters: {
                      '$seriesId': episode.getTypedValue('seriesId'),
                      '$seasonId': episode.getTypedValue('seasonId'),
                      '$episodeId': episode.getTypedValue('episodeId')
                  },
                  text: `
                      DECLARE $seriesId AS Uint64;
                      DECLARE $seasonId AS Uint64;
                      DECLARE $episodeId AS Uint64;

                      SELECT title,
                             air_date
                      FROM episodes
                      WHERE series_id = $seriesId
                        AND season_id = $seasonId
                        AND episode_id = $episodeId;`
              });
              const {value: resultSet} = await resultSets.next();
              const {value: row} = await resultSet.rows.next();
              await opFinished;
              logger.info(`Parametrized select query ${JSON.stringify(row, null, 2)}`);
          }
      }
  });
}

Управление транзакциями

Выполняются вызовы операторов управления транзакциями TCL - Begin и Commit.

В большинстве случаев вместо явного использования вызовов Begin и Commit лучше использовать параметры контроля транзакций в вызовах execute. Это позволит избежать лишних обращений к YDB и эффективней выполнять запросы.

Фрагмент кода, демонстрирующий явное использование вызовов Session.beginTransaction() и Session.сommitTransaction() для выполнения транзакции:

async function explicitTcl(driver: Driver, ids: ThreeIds, logger: Logger) {
    logger.info('Running prepared query with explicit transaction control...');
    await driver.queryClient.do({
        fn: async (session) => {
            await session.beginTransaction({serializableReadWrite: {}});
            const [seriesId, seasonId, episodeId] = ids;
            const episode = new Episode({seriesId, seasonId, episodeId, title: '', airDate: new Date()});
            await session.execute({
                parameters: {
                    '$seriesId': episode.getTypedValue('seriesId'),
                    '$seasonId': episode.getTypedValue('seasonId'),
                    '$episodeId': episode.getTypedValue('episodeId')
                },
                text: `
                    DECLARE $seriesId AS Uint64;
                    DECLARE $seasonId AS Uint64;
                    DECLARE $episodeId AS Uint64;
  
                    UPDATE episodes
                    SET air_date = CurrentUtcDate()
                    WHERE series_id = $seriesId
                      AND season_id = $seasonId
                      AND episode_id = $episodeId;`
            })
            const txId = session.txId;
            await session.commitTransaction();
            logger.info(`TxId ${txId} committed.`);
        }
    });
}
async function transactionPerWholeDo(driver: Driver, ids: ThreeIds, logger: Logger) {
    logger.info('Running query with one transaction per whole doTx()...');
    await driver.queryClient.doTx({
        txSettings: {serializableReadWrite: {}},
        fn: async (session) => {
            const [seriesId, seasonId, episodeId] = ids;
            const episode = new Episode({seriesId, seasonId, episodeId, title: '', airDate: new Date()});
            await session.execute({
                parameters: {
                    '$seriesId': episode.getTypedValue('seriesId'),
                    '$seasonId': episode.getTypedValue('seasonId'),
                    '$episodeId': episode.getTypedValue('episodeId')
                },
                text: `
                    DECLARE $seriesId AS Uint64;
                    DECLARE $seasonId AS Uint64;
                    DECLARE $episodeId AS Uint64;

                    UPDATE episodes
                    SET air_date = CurrentUtcDate()
                    WHERE series_id = $seriesId
                      AND season_id = $seasonId
                      AND episode_id = $episodeId;`
            })
            logger.info(`TxId ${session.txId} will be committed by doTx().`);
        }
    });
}

Обработка ошибок

Подробно об обработке ошибок написано в разделе Обработка ошибок в API.

Предыдущая
Следующая