Key-Based Data Access¶
Important
Transition API: The Key-Based Data Access API is provided primarily to ease migration from FDBRecordStore API usage to SQL-based queries. While it remains supported, it is not the recommended approach for new applications. For most use cases, SQL queries provide better flexibility, optimization, and maintainability. This API may evolve or be deprecated in future versions as the SQL interface matures.
The Key-Based Data Access API provides programmatic methods for common database operations without writing SQL. These operations work directly with primary keys and support:
Scan: Range queries over primary key prefixes
Get: Single record lookup by complete primary key
Insert: Programmatic record insertion with
RelationalStructDelete: Key-based deletion (single, batch, and range)
For more complex queries, filtering, joins, or aggregations, use the SQL query language instead.
Getting Started¶
The Key-Based Data Access methods are available on RelationalStatement, which extends the standard JDBC Statement. To access these methods, unwrap your statement:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
// Unwrap to RelationalStatement to access direct access methods
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Now you can use executeScan, executeGet, executeInsert, executeDelete
}
}
Scan Operations¶
Scans retrieve multiple records that match a primary key prefix. The API supports pagination through continuations for handling large result sets.
Basic Scan¶
To scan all records in a table, use an empty KeySet:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Scan all records in the products table
KeySet emptyKey = new KeySet();
try (RelationalResultSet rs = relStmt.executeScan("products", emptyKey, Options.NONE)) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
System.out.println("Product: " + id + " - " + name);
}
}
}
}
Scan with Key Prefix¶
Narrow the scan to records matching a primary key prefix. The key columns must form a contiguous prefix of the primary key:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Scan products with a specific category (assuming category is part of the key)
KeySet keyPrefix = new KeySet()
.setKeyColumn("category", "Electronics");
try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, Options.NONE)) {
while (rs.next()) {
String name = rs.getString("name");
long price = rs.getLong("price");
System.out.println(name + ": $" + price);
}
}
}
}
Key Prefix Rules:
Given a table with PRIMARY KEY(a, b, c):
✓ Valid:
{a: 1},{a: 1, b: 2},{a: 1, b: 2, c: 3}✗ Invalid:
{b: 2}(not a prefix),{a: 1, c: 3}(skipsb)
Scan with Continuation¶
For large result sets, use continuations to paginate through results:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
KeySet keyPrefix = new KeySet();
int batchSize = 10;
Continuation continuation = null;
// Initial scan without continuation
Options options = Options.builder()
.withOption(Options.Name.MAX_ROWS, batchSize)
.build();
int totalRecords = 0;
try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) {
while (rs.next()) {
// Process first batch
System.out.println("Product: " + rs.getString("name"));
totalRecords++;
}
continuation = rs.getContinuation();
}
// Continue scanning with continuation
while (!continuation.atEnd()) {
Options contOptions = Options.builder()
.withOption(Options.Name.CONTINUATION, continuation)
.withOption(Options.Name.MAX_ROWS, batchSize)
.build();
int rowCount = 0;
try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, contOptions)) {
while (rs.next()) {
// Process record
System.out.println("Product: " + rs.getString("name"));
rowCount++;
}
// Get continuation for next batch
continuation = rs.getContinuation();
}
totalRecords += rowCount;
System.out.println("Processed " + rowCount + " records in this batch");
}
System.out.println("Total records processed: " + totalRecords);
}
}
The continuation points to the first unread row after the current position in the ResultSet.
Get Operations¶
Get operations retrieve a single record by its complete primary key. The result set will contain either 0 or 1 row.
Basic Get¶
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Get a single record by primary key
KeySet primaryKey = new KeySet()
.setKeyColumn("id", 1L);
try (RelationalResultSet rs = relStmt.executeGet("products", primaryKey, Options.NONE)) {
if (rs.next()) {
String name = rs.getString("name");
long price = rs.getLong("price");
System.out.println("Found: " + name + " - $" + price);
} else {
System.out.println("Product not found");
}
}
}
}
Get with Composite Key¶
For tables with composite primary keys, provide all key columns:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Get with composite primary key
KeySet primaryKey = new KeySet()
.setKeyColumn("store_id", 100L)
.setKeyColumn("product_id", 1L);
try (RelationalResultSet rs = relStmt.executeGet("inventory", primaryKey, Options.NONE)) {
if (rs.next()) {
int stock = rs.getInt("quantity");
System.out.println("Stock level: " + stock);
}
}
}
}
Important: Unlike scans, Get requires a complete primary key. Incomplete keys will cause an error.
Insert Operations¶
Insert operations add records to a table using RelationalStruct objects. The struct must contain values for all required columns.
Single Insert¶
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Build a record to insert
RelationalStruct product = EmbeddedRelationalStruct.newBuilder()
.addLong("id", 100L)
.addString("name", "New Widget")
.addString("category", "Electronics")
.addLong("price", 299L)
.addInt("stock", 50)
.build();
int rowsInserted = relStmt.executeInsert("products", product);
System.out.println("Inserted " + rowsInserted + " row(s)");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Build a record to insert
RelationalStruct product = JDBCRelationalStruct.newBuilder()
.addLong("id", 100L)
.addString("name", "New Widget")
.addString("category", "Electronics")
.addLong("price", 299L)
.addInt("stock", 50)
.build();
int rowsInserted = relStmt.executeInsert("products", product);
System.out.println("Inserted " + rowsInserted + " rows");
conn.commit();
}
}
Batch Insert¶
Insert multiple records in a single operation for better performance:
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Build multiple records
List<RelationalStruct> products = new ArrayList<>();
products.add(EmbeddedRelationalStruct.newBuilder()
.addLong("id", 101L)
.addString("name", "Widget A")
.addString("category", "Electronics")
.addLong("price", 199L)
.addInt("stock", 25)
.build());
products.add(EmbeddedRelationalStruct.newBuilder()
.addLong("id", 102L)
.addString("name", "Widget B")
.addString("category", "Electronics")
.addLong("price", 249L)
.addInt("stock", 30)
.build());
int rowsInserted = relStmt.executeInsert("products", products);
System.out.println("Inserted " + rowsInserted + " row(s)");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
List<RelationalStruct> products = new ArrayList<>();
products.add(JDBCRelationalStruct.newBuilder()
.addLong("id", 101L)
.addString("name", "Laptop")
.addString("category", "Electronics")
.addLong("price", 999L)
.addInt("stock", 25)
.build());
products.add(JDBCRelationalStruct.newBuilder()
.addLong("id", 102L)
.addString("name", "Mouse")
.addString("category", "Electronics")
.addLong("price", 29L)
.addInt("stock", 100)
.build());
int rowsInserted = relStmt.executeInsert("products", products);
System.out.println("Batch inserted " + rowsInserted + " rows");
conn.commit();
}
}
Insert with Replace on Duplicate¶
Use the REPLACE_ON_DUPLICATE_PK option to update existing records instead of failing on primary key conflicts:
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
RelationalStruct product = EmbeddedRelationalStruct.newBuilder()
.addLong("id", 1L)
.addString("name", "Updated Widget")
.addString("category", "Electronics")
.addLong("price", 199L)
.addInt("stock", 100)
.build();
// Replace if the primary key already exists
Options options = Options.builder()
.withOption(Options.Name.REPLACE_ON_DUPLICATE_PK, true)
.build();
int rowsInserted = relStmt.executeInsert("products", product, options);
System.out.println("Inserted/replaced " + rowsInserted + " row(s)");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
RelationalStruct product = JDBCRelationalStruct.newBuilder()
.addLong("id", 100L) // This might conflict with existing record
.addString("name", "Updated Widget")
.addString("category", "Electronics")
.addLong("price", 349L)
.addInt("stock", 75)
.build();
Options options = Options.builder()
.withOption(Options.Name.REPLACE_ON_DUPLICATE_PK, true)
.build();
int rowsInserted = relStmt.executeInsert("products", product, options);
System.out.println("Inserted/updated " + rowsInserted + " rows");
conn.commit();
}
}
Delete Operations¶
Delete operations remove records by their primary keys. Three variants are supported: single delete, batch delete, and range delete.
Single Delete¶
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Delete by primary key
KeySet primaryKey = new KeySet()
.setKeyColumn("id", 100L);
List<KeySet> keysToDelete = List.of(primaryKey);
int rowsDeleted = relStmt.executeDelete("products", keysToDelete);
System.out.println("Deleted " + rowsDeleted + " row(s)");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
Batch Delete¶
Delete multiple records by providing a collection of keys:
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Delete multiple records by their keys
List<KeySet> keysToDelete = List.of(
new KeySet().setKeyColumn("id", 101L),
new KeySet().setKeyColumn("id", 102L),
new KeySet().setKeyColumn("id", 103L)
);
int rowsDeleted = relStmt.executeDelete("products", keysToDelete);
System.out.println("Deleted " + rowsDeleted + " row(s)");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
Range Delete¶
Delete all records matching a primary key prefix using executeDeleteRange:
try (Connection conn = DriverManager.getConnection(url)) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Delete all products in a specific category (assuming category is part of key)
KeySet keyPrefix = new KeySet()
.setKeyColumn("category", "Discontinued");
relStmt.executeDeleteRange("products", keyPrefix, Options.NONE);
System.out.println("Deleted all discontinued products");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
}
Warning: Range deletes can affect many records. Use with caution.
Working with KeySet¶
KeySet specifies primary key values for direct access operations. It supports fluent builder-style construction:
KeySet key = new KeySet()
.setKeyColumn("column1", value1)
.setKeyColumn("column2", value2);
Key Points:
Column names are case-sensitive and should match your schema
For scans and range deletes, provide a contiguous key prefix
For gets and point deletes, provide the complete primary key
Use
KeySet.EMPTYornew KeySet()to match all records (full table scan)
Options¶
The Options class configures execution behavior for direct access operations. Common options include:
CONTINUATION¶
Specifies where to resume a scan operation (see continuation example above).
INDEX_HINT¶
Suggests a specific index for FRL to use:
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
RelationalStatement relStmt = stmt.unwrap(RelationalStatement.class);
// Use a specific index for the scan
Options options = Options.builder()
.withOption(Options.Name.INDEX_HINT, "products_by_category")
.build();
KeySet keyPrefix = new KeySet()
.setKeyColumn("category", "Electronics");
try (RelationalResultSet rs = relStmt.executeScan("products", keyPrefix, options)) {
while (rs.next()) {
System.out.println("Product: " + rs.getString("name"));
}
}
}
}
MAX_ROWS¶
Limits the number of rows returned in a single scan before prompting for continuation:
Options options = Options.builder()
.withOption(Options.Name.MAX_ROWS, 100)
.build();
REPLACE_ON_DUPLICATE_PK¶
When inserting, replaces existing records with conflicting primary keys instead of failing (see insert example above).
Building Options¶
Use the builder pattern to construct options:
Options options = Options.builder()
.withOption(Options.Name.INDEX_HINT, "my_index")
.withOption(Options.Name.MAX_ROWS, 50)
.build();
// Or use Options.NONE for default behavior
relStmt.executeScan("table", keySet, Options.NONE);
See Also¶
JDBC Guide - Basic JDBC usage patterns
Advanced JDBC Features - Working with STRUCTs and ARRAYs
SQL Reference - Complete SQL syntax reference
Databases, Schemas, Schema Templates - Understanding the data model