プリミティブ CRUD インターフェースを介して非トランザクションストレージ操作を実行する
このページは英語版のページが機械翻訳されたものです。英語版との間に矛盾または不一致がある場合は、英語版を正としてください。
このページでは、プリミティブ CRUD インターフェース (Storage API とも呼ばれる) を通じて非トランザクションストレージ操作を実行する方法について説明します。このガイドでは、読者が ScalarDB について高度な知識を持っていることを前提としています。
既存のストレージおよびデータベースシステム上でストレージに依存しない、またはデータベースに依存しない ACID トランザクションを実現するためのキーの1つは、ScalarDB が提供するストレージ抽象化機能です。ストレージ抽象化は、データモデル と、データモデルに基づいて操作を発行する API (Storage API) を定義します。
ほとんどの場合、Transactional API を使用することになりますが、別のオプションとして Storage API を使用することもできます。
Storage API を使用する利点は次のとおりです。
- トランザクション API と同様に、基盤となるストレージ実装についてあまり気にせずにアプリケーションコードを作成できます。
- アプリケーション内の一部のデータに対してトランザクションが必要ない場合は、Storage API を使用してトランザクションを部分的に回避し、実行を高速化できます。
ストレージ API を直接使用したり、トランザクション API とストレージ API を混在させたりした場合、予期しない動作が発生する可能性があります。たとえば、ストレージ API はトランザクション機能を提供できないため、操作の実行時に障害が発生すると、API によって異常やデータの不整合が発生する可能性があります。
したがって、ストレージ API の使用には 非常に 注意し、何をしているのかを正確に理解している場合にのみ使用してください。
ストレージ API の例
このセクションでは、基本的な電子マネーアプリケーションでストレー ジ API を使用する方法について説明します。
この例では電子マネーアプリケーションが簡略化されており、実稼働環境には適していません。
ScalarDB の設定
開始する前に、ScalarDB をはじめよう で説明されているのと同じ方法で ScalarDB を設定する必要があります。
これを念頭に置いて、このストレージ API の例では、設定ファイル scalardb.properties が存在することを前提としています。
データベーススキーマの設定
アプリケーションでデータベーススキーマ (データを整理する方法) を定義する必要があります。サポートされているデータ型の詳細については、ScalarDB と他のデータベース間のデータ型マッピングを参照してください。
この例では、scalardb/docs/getting-started ディレクトリに emoney-storage.json という名前のファイルを作成します。次に、次の JSON コードを追加してスキーマを定義します。
次の JSON では、transaction フィールドが false に設定されており、このテーブルを Storage API で使用する必要があることを示しています。
{
"emoney.account": {
"transaction": false,
"partition-key": [
"id"
],
"clustering-key": [],
"columns": {
"id": "TEXT",
"balance": "INT"
}
}
}
スキーマを適用するには、ScalarDB Releases ページに移動し、使用している ScalarDB のバージョンに一致する ScalarDB Schema Loader を getting-started フォルダーにダウンロードします。
次に、<VERSION> をダウンロードした ScalarDB Schema Loader のバージョンに置き換えて、次のコマンドを実行します。
java -jar scalardb-schema-loader-<VERSION>.jar --config scalardb.properties -f emoney-storage.json
サンプルコード
以下は、Storage API を使用する電子マネーアプリケーションのサンプルソースコードです。
前述のとおり、Storage API はトランザクション機能を提供できないため、操作の実行中に障害が発生すると、API によって異常やデータの不整合が発生する可能性があります。したがって、Storage API の使用 には十分注意し、何をしているのかを正確に理解している場合にのみ使用してください。
public class ElectronicMoney {
private static final String SCALARDB_PROPERTIES =
System.getProperty("user.dir") + File.separator + "scalardb.properties";
private static final String NAMESPACE = "emoney";
private static final String TABLENAME = "account";
private static final String ID = "id";
private static final String BALANCE = "balance";
private final DistributedStorage storage;
public ElectronicMoney() throws IOException {
StorageFactory factory = StorageFactory.create(SCALARDB_PROPERTIES);
storage = factory.getStorage();
}
public void charge(String id, int amount) throws ExecutionException {
// Retrieve the current balance for id
Get get =
Get.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, id))
.build();
Optional<Result> result = storage.get(get);
// Calculate the balance
int balance = amount;
if (result.isPresent()) {
int current = result.get().getInt(BALANCE);
balance += current;
}
// Update the balance
Put put =
Put.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, id))
.intValue(BALANCE, balance)
.build();
storage.put(put);
}
public void pay(String fromId, String toId, int amount) throws ExecutionException {
// Retrieve the current balances for ids
Get fromGet =
Get.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, fromId))
.build();
Get toGet =
Get.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, toId))
.build();
Optional<Result> fromResult = storage.get(fromGet);
Optional<Result> toResult = storage.get(toGet);
// Calculate the balances (it assumes that both accounts exist)
int newFromBalance = fromResult.get().getInt(BALANCE) - amount;
int newToBalance = toResult.get().getInt(BALANCE) + amount;
if (newFromBalance < 0) {
throw new RuntimeException(fromId + " doesn't have enough balance.");
}
// Update the balances
Put fromPut =
Put.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, fromId))
.intValue(BALANCE, newFromBalance)
.build();
Put toPut =
Put.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, toId))
.intValue(BALANCE, newToBalance)
.build();
storage.put(fromPut);
storage.put(toPut);
}
public int getBalance(String id) throws ExecutionException {
// Retrieve the current balances for id
Get get =
Get.newBuilder()
.namespace(NAMESPACE)
.table(TABLENAME)
.partitionKey(Key.ofText(ID, id))
.build();
Optional<Result> result = storage.get(get);
int balance = -1;
if (result.isPresent()) {
balance = result.get().getInt(BALANCE);
}
return balance;
}
public void close() {
storage.close();
}
}
ストレージ API ガイド
ストレージ API は、管理 API と CRUD API で設定されています。
管理 API
このセクションで説明するように、管理操作をプログラムで実行できます。
管理操作を実行するために使用できる別の方法は、Schema Loader を使用することです。
DistributedStorageAdmin インスタンスを取得する
管理操作を実行するには、まず DistributedStorageAdmin インスタンスを取得する必要があります。次のように StorageFactory から DistributedStorageAdmin インスタンスを取得できます。
StorageFactory storageFactory = StorageFactory.create("<CONFIGURATION_FILE_PATH>");
DistributedStorageAdmin admin = storageFactory.getStorageAdmin();
設定の詳細については、ScalarDB 設定を参照してください。
すべての管理操作を実行したら、次のように DistributedStorageAdmin インスタンスを閉じる必要があります。
admin.close();
名前空間を作成する
テーブルは1つの名前空間に属するため、テーブルを作成する前に名前空間を作成する必要があります。
名前空間は次のように作成できます。
// Create the namespace "ns". If the namespace already exists, an exception will be thrown.
admin.createNamespace("ns");
// Create the namespace only if it does not already exist.
boolean ifNotExists = true;
admin.createNamespace("ns", ifNotExists);
// Create the namespace with options.
Map<String, String> options = ...;
admin.createNamespace("ns", options);
作成オプションの詳細については、作成オプションを参照してください。
テーブルを作成する
テーブルを作成するときは、テーブルメタデータを定義してからテーブルを作成する必要があります。
テーブルメタデータを定義するには、TableMetadata を使用できます。次に、テーブルの列、パーティションキー、クラスタリングキー (クラスタリング順序を含む)、およびセカンダリインデックスを定義する方法を示します。
// Define the table metadata.
TableMetadata tableMetadata =
TableMetadata.newBuilder()
.addColumn("c1", DataType.INT)
.addColumn("c2", DataType.TEXT)
.addColumn("c3", DataType.BIGINT)
.addColumn("c4", DataType.FLOAT)
.addColumn("c5", DataType.DOUBLE)
.addPartitionKey("c1")
.addClusteringKey("c2", Scan.Ordering.Order.DESC)
.addClusteringKey("c3", Scan.Ordering.Order.ASC)
.addSecondaryIndex("c4")
.build();
ScalarDB のデータモデルの詳細については、データモデルを参照してください。
次に、次のようにテーブルを作成します。
// Create the table "ns.tbl". If the table already exists, an exception will be thrown.
admin.createTable("ns", "tbl", tableMetadata);
// Create the table only if it does not already exist.
boolean ifNotExists = true;
admin.createTable("ns", "tbl", tableMetadata, ifNotExists);
// Create the table with options.
Map<String, String> options = ...;
admin.createTable("ns", "tbl", tableMetadata, options);
作成オプションの詳細については、作成オプションを参照してください。
セカンダリインデックスを作成する
セカンダリインデックスは次のように作成できます。
// Create a secondary index on column "c5" for table "ns.tbl". If a secondary index already exists, an exception will be thrown.
admin.createIndex("ns", "tbl", "c5");
// Create the secondary index only if it does not already exist.
boolean ifNotExists = true;
admin.createIndex("ns", "tbl", "c5", ifNotExists);
// Create the secondary index with options.
Map<String, String> options = ...;
admin.createIndex("ns", "tbl", "c5", options);
作成オプションの詳細については、作成オプションを参照してください。
テーブルに新しい列を追加する
次のように、テーブルに新しい非パーティションキー列を追加できます。
// Add a new column "c6" with the INT data type to the table "ns.tbl".
admin.addNewColumnToTable("ns", "tbl", "c6", DataType.INT)
テーブルに新しい列を追加する場合は、基盤となるストレージによって実行時間が大きく異なる可能性があるため、慎重に検討する必要があります。特にデータベースが本番環境で実行されている場合は、以下の点を考慮して適切に計画を立ててください。
- Cosmos DB for NoSQL および DynamoDB の場合: テーブルスキーマは変更されないため、列の追加はほぼ瞬時に行われます。別のテーブルに格納されているテーブルメタデータのみが更新されます。
- Cassandra の場合: 列を追加すると、スキーマメタデータのみが更新され、既存のスキーマレコードは変更されません。クラスタートポロジが実行時間の主な要因です。スキーマメタデータの変更は、ゴシッププロトコルを介して各クラスターノードに共有されます。このため、クラスターが大きいほど、すべてのノードが更新されるまでの時間が長くなります。
- リレーショナルデータベース (MySQL、Oracle など) の場合: 列の追加は実行にそれほど時 間がかかりません。
テーブルを切り捨てる
テーブルを切り捨てるには、次のようにします。
// Truncate the table "ns.tbl".
admin.truncateTable("ns", "tbl");
セカンダリインデックスを削除する
セカンダリインデックスは次のように削除できます。
// Drop the secondary index on column "c5" from table "ns.tbl". If the secondary index does not exist, an exception will be thrown.
admin.dropIndex("ns", "tbl", "c5");
// Drop the secondary index only if it exists.
boolean ifExists = true;
admin.dropIndex("ns", "tbl", "c5", ifExists);
テーブルを削除する
テーブルを削除するには、次のようにします。
// Drop the table "ns.tbl". If the table does not exist, an exception will be thrown.
admin.dropTable("ns", "tbl");
// Drop the table only if it exists.
boolean ifExists = true;
admin.dropTable("ns", "tbl", ifExists);
名前空間を削除する
名前空間を削除するには、次のようにします。
// Drop the namespace "ns". If the namespace does not exist, an exception will be thrown.
admin.dropNamespace("ns");
// Drop the namespace only if it exists.
boolean ifExists = true;
admin.dropNamespace("ns", ifExists);
既存の名前空間を取得する
既存の名前空間は次のように取得できます。
Set<String> namespaces = admin.getNamespaceNames();
名前空間のテーブルを取得する
名前空間のテーブルは次のように取得できます。
// Get the tables of the namespace "ns".
Set<String> tables = admin.getNamespaceTableNames("ns");
テーブルメタデータを取得する
テーブルメタデータは次のように取得できます。
// Get the table metadata for "ns.tbl".
TableMetadata tableMetadata = admin.getTableMetadata("ns", "tbl");
名前空間を修復する
名前空間が不明な状態の場合 (名前空間が基盤となるストレージに存在するが ScalarDB メタデータが存在しない、またはその逆)、このメソッドは必要に応じて名前空間とそのメタデータを再作成します。
名前空間は次のように修復できます。
// Repair the namespace "ns" with options.
Map<String, String> options = ...;
admin.repairNamespace("ns", options);
テーブルを修復する
テーブルが不明な状態の場合 (テーブルは基盤となるストレージに存在するが ScalarDB メタデータは存在しない、またはその逆)、このメソッドは必要に応じてテーブル、そのセカンダリインデックス、およびそのメタデータを再作成します。
テーブルは次のように修復できます。
// Repair the table "ns.tbl" with options.
TableMetadata tableMetadata =
TableMetadata.newBuilder()
...
.build();
Map<String, String> options = ...;
admin.repairTable("ns", "tbl", tableMetadata, options);
最新の ScalarDB API をサポートするように環境をアップグレードする
ScalarDB API の最新バージョンをサポートするように ScalarDB 環境をアップグレードできます。通常、リリースノートに記載されているように、アプリケーション環境が使用する ScalarDB バージョンを更新した後、このメソッドを実行する必要があります。
// Upgrade the ScalarDB environment.
Map<String, String> options = ...;
admin.upgrade(options);
CRUD 操作を実装する
次のセクションでは、CRUD 操作について説明します。
DistributedStorage インスタンスを取得する
Storage API で CRUD 操作を実行するには、DistributedStorage インスタンスを取得する必要があります。
インスタンスは次のように取得できます。
StorageFactory storageFactory = StorageFactory.create("<CONFIGURATION_FILE_PATH>");
DistributedStorage storage = storageFactory.getStorage();
すべての CRUD 操作を実行したら、次のように DistributedStorage インスタンスを閉じる必要があります。
storage.close();
Get 操作
Get は、主キーで指定された単一のレコードを取得する操作です。
まず Get オブジェクトを作成し、次に次のように storage.get() メソッドを使用してオブジェクトを実行する必要があります。
// Create a `Get` operation.
Key partitionKey = Key.ofInt("c1", 10);
Key clusteringKey = Key.of("c2", "aaa", "c3", 100L);
Get get =
Get.newBuilder()
.namespace("ns")
.table("tbl")
.partitionKey(partitionKey)
.clusteringKey(clusteringKey)
.projections("c1", "c2", "c3", "c4")
.build();
// Execute the `Get` operation.
Optional<Result> result = storage.get(get);
また、投影を指定して、返される列を選択することもできます。
Key オブジェクトの構築方法の詳細については、キーの構築を参照してください。また、Result オブジェクトの処理方法の詳細については、Result オブジェクトの処理を参照してください。
一貫性レベルを指定する
Storage API の各操作 (Get、Scan、Put、Delete) で一貫性レベルを次のように指定できます。
Get get =
Get.newBuilder()
.namespace("ns")
.table("tbl")
.partitionKey(partitionKey)
.clusteringKey(clusteringKey)
.consistency(Consistency.LINEARIZABLE) // Consistency level
.build();
次の表は、3つの一貫性レベルについて説明しています。
| 一貫性レベル | 説明 |
|---|---|
SEQUENTIAL | 順次一貫性は、基礎となるストレージ実装によってすべての操作が何らかの順次順序で実行されるようにされ、各プロセスの操作がこの順序で実行されることを前提としています。 |
EVENTUAL | 結果一貫性は、基礎となるストレージ実装によってすべての操作が最終的に実行されることを前提としています。 |
LINEARIZABLE | 線形化可能な一貫性は、基礎となるストレージ実装によって各操作が呼び出しから完了までの間のある時点でアトミックに実行されるようにされるこ とを前提としています。 |
セカンダリインデックスを使用して Get を実行する
セカンダリインデックスを使用して Get 操作を実行できます。
パーティションキーを指定する代わりに、次のようにインデックスキー (インデックス付き列) を指定してセカンダリインデックスを使用できます。
// Create a `Get` operation by using a secondary index.
Key indexKey = Key.ofFloat("c4", 1.23F);
Get get =
Get.newBuilder()
.namespace("ns")
.table("tbl")
.indexKey(indexKey)
.projections("c1", "c2", "c3", "c4")
.build();
// Execute the `Get` operation.
Optional<Result> result = storage.get(get);
結果に複数のレコードがある場合、storage.get() は例外をスローします。
Scan 操作
Scan は、パーティション内の複数のレコードを取得する操作です。Scan 操作では、クラスタリングキーの境界とクラスタリングキー列の順序を指定できます。
まず Scan オブジェクトを作成し、次に次のように storage.scan() メソッドを使用してオブジェクトを実行する必要があります。
// Create a `Scan` operation.
Key partitionKey = Key.ofInt("c1", 10);
Key startClusteringKey = Key.of("c2", "aaa", "c3", 100L);
Key endClusteringKey = Key.of("c2", "aaa", "c3", 300L);
Scan scan =
Scan.newBuilder()
.namespace("ns")
.table("tbl")
.partitionKey(partitionKey)
.start(startClusteringKey, true) // Include startClusteringKey
.end(endClusteringKey, false) // Exclude endClusteringKey
.projections("c1", "c2", "c3", "c4")
.orderings(Scan.Ordering.desc("c2"), Scan.Ordering.asc("c3"))
.limit(10)
.build();
// Execute the `Scan` operation.
Scanner scanner = storage.scan(scan);
クラスタリングキー境界 を省略するか、start 境界または end 境界のいずれかを指定できます。orderings を指定しない場合は、テーブルの作成時に定義したクラスタリング順序で結果が並べられます。
さらに、projections を指定して返される列を選択し、limit を使用して Scan 操作で返されるレコードの数を指定できます。
Scanner オブジェクトの処理
Storage API の Scan 操作は Scanner オブジェクトを返します。
Scanner オブジェクトから結果を1つずつ取得する場合は、次のように one() メソッドを使用できます。
Optional<Result> result = scanner.one();
または、すべての結果のリストを取得する場合は、次のように all() メソッドを使用できます。
List<Result> results = scanner.all();
さらに、Scanner は Iterable を実装しているので、次のように for-each ループ内で Scanner を使用できます。
for (Result result : scanner) {
...
}
結果を取得した後は、Scanner オブジェクトを閉じることを忘れないでください。
scanner.close();
または、次のように try-with-resources を使用することもできます。
try (Scanner scanner = storage.scan(scan)) {
...
}