Example app in Node.js
This page provides a detailed description of the code for
an example application,
which is available as part of the YDB Node.js SDK.
Initializing a database connection
To interact with YDB, create instances of the driver, client, and session:
- The YDB driver facilitates interaction between the app and YDB nodes at the transport layer. It must be initialized before creating a client or session and must persist throughout the YDB access lifecycle.
- The YDB client operates on top of the YDB driver and enables the handling of entities and transactions.
- The YDB session, which is part of the YDB client context, contains information about executed transactions and prepared queries.
App code snippet for driver initialization:
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);
}
App code snippet for creating a session:
const result = await driver.queryClient.do({
...
fn: async (session) => {
...
}
});
Creating tables
Create tables to be used in operations on a test app. This step results in the creation of database tables for the series directory data model:
Series
Seasons
Episodes
After the tables are created, a method for retrieving information about data schema objects is called, and the result of its execution is displayed.
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)
);`,
});
},
});
}
Adding data
Add data to the created tables using the UPSERT
statement in YQL. A data update request is sent to the server as a single request with transaction auto-commit mode enabled.
Code snippet for data insert/update:
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.')
}
Retrieving data
Retrieve data using a SELECT
statement in YQL. Handle the retrieved data selection in the app.
The QuerySession.execute()
method is used to execute YQL queries.
Depending on the rowMode parameter, the data can be retrieved in javascript form or as YDB structures.
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)}`);
}
Parameterized queries
Query data using parameters. This query execution method is preferable because it allows the server to reuse the query execution plan for subsequent calls and protects against vulnerabilities such as 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)}`);
}
}
});
}
Scan queries
Execute a scan query to produce a data stream. Streaming allows to read an unlimited number of rows and an unlimited amount of data.
The QuerySession.execute()
method is used to retrieve data in a streaming manner.
async function selectWithParametrs(driver: Driver, data: ThreeIds[], logger: Logger): Promise<void> {
logger.info('Selecting with a parametrized 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)}`);
}
}
});
}
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 allows to avoid additional requests to YDB server and thus run queries more efficiently.
Here's a code sample demonstrating how to explicitly use the Session.beginTransaction()
and Session.commitTransaction()
calls to execute a transaction:
async function explicitTcl(driver: Driver, ids: ThreeIds, logger: Logger) {
logger.info('Running a parametrized 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 a 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().`);
}
});
}
Error handling
For more information about error handling, see Error handling in the API.