1. 概要

このチュートリアルでは、SirixDBとは何かとその最も重要な設計目標の概要を説明します。

次に、低レベルのカーソルベースのトランザクションAPIについて説明します。

2. SirixDBの機能

SirixDBは、ログ構造化された一時的な NoSQL ドキュメントストアであり、進化的なデータを格納します。 ディスク上のデータを上書きすることはありません。 したがって、データベース内のリソースの完全なリビジョン履歴を効率的に復元してクエリすることができます。 SirixDBは、新しいリビジョンごとに最小限のストレージオーバーヘッドが作成されることを保証します。

現在、SirixDBは、バイナリXMLストアとJSONストアの2つの組み込みネイティブデータモデルを提供しています。

2.1. 設計目標

最も重要なコア原則と設計目標のいくつかは次のとおりです。

  • 同時実行性– SirixDBにはロックがほとんど含まれておらず、マルチスレッドシステムに可能な限り適していることを目指しています
  • 非同期RESTAPI–操作は独立して実行できます。 各トランザクションは特定のリビジョンにバインドされ、リソース上の1つの読み取り/書き込みトランザクションのみがN個の読み取り専用トランザクションに同時に許可されます
  • バージョン管理/改訂履歴– SirixDBは、ストレージオーバーヘッドを最小限に抑えながら、データベース内のすべてのリソースの改訂履歴を保存します。 読み取りと書き込みのパフォーマンスは調整可能です。 これは、リソースを作成するために指定できるバージョン管理タイプによって異なります。
  • データの整合性– SirixDBは、ZFSと同様に、ページの完全なチェックサムを親ページに格納します。 つまり、SirixDB開発者は将来データベースを分割して複製することを目指しているため、将来の読み取り時にほぼすべてのデータ破損を検出できるということです。
  • コピーオンライトセマンティクス– ファイルシステムBtrfsおよびZFSと同様に、SirixDBはCoWセマンティクスを使用します。つまり、SirixDBはデータを上書きしません。 代わりに、データベースページのフラグメントがコピーされ、新しい場所に書き込まれます
  • リビジョンごとおよびレコードごとのバージョン管理– SirixDBは、ページごとだけでなく、レコードごとにもバージョン管理を行います。 したがって、データページ内のレコードの潜在的に小さな部分を変更するときはいつでも、ページ全体をコピーして、ディスクまたはフラッシュドライブの新しい場所に書き込む必要はありません。 代わりに、データベースリソースの作成中に、バックアップシステムまたはスライディングスナップショットアルゴリズムから知られているいくつかのバージョン管理戦略の1つを指定できます。 指定するバージョン管理タイプは、SirixDBがデータページをバージョン管理するために使用します
  • 保証されたアトミック性(WALなし)– システムが一貫性のない状態になることはありません(ハードウェア障害がない限り)。つまり、予期しない電源オフによってシステムが損傷することはありません。 これは、先行書き込みログ( WAL )のオーバーヘッドなしで実現されます。
  • ログ構造でSSDに対応– SirixDBは、コミット中にすべてをフラッシュドライブに順番に書き込み、同期します。 コミットされたデータを上書きすることはありません

今後の記事で焦点をより高いレベルに切り替える前に、まずJSONデータで例示された低レベルのAPIを紹介したいと思います。 たとえば、XMLデータベースとJSONデータベースの両方をクエリするためのXQuery-API、または非同期の一時的なRESTfulAPIです。 基本的に、XMLリソースの保存、トラバース、比較にも微妙な違いがある同じ低レベルAPIを使用できます。

SirixDBを使用するには、少なくともJava11を使用する必要があります。

3. SirixDBを埋め込むためのMavenの依存関係

例に従うには、最初に sirix-core依存関係を含める必要があります。たとえば、Mavenを使用します。

<dependency>
    <groupId>io.sirix</groupId>
    <artifactId>sirix-core</artifactId>
    <version>0.9.3</version>
</dependency>

またはGradle経由:

dependencies {
    compile 'io.sirix:sirix-core:0.9.3'
}

4. SirixDBでのツリーエンコーディング

SirixDBのノードは、 firstChild / leftSibling / rightSibling / parentNodeKey /nodeKeyエンコーディングによって他のノードを参照します。

図の番号は、単純な連続番号ジェネレーターで生成された、自動生成された一意の安定したノードIDです。

すべてのノードには、最初の子、左の兄弟、右の兄弟、および親ノードがあります。 さらに、SirixDBは、各ノードの子の数、子孫の数、およびハッシュを格納できます。

次のセクションでは、SirixDBのコア低レベルJSONAPIを紹介します。

5. 単一のリソースでデータベースを作成する

まず、単一のリソースでデータベースを作成する方法を示します。 リソースはJSONファイルからインポートされ、SirixDBの内部バイナリ形式で永続的に保存されます。

var pathToJsonFile = Paths.get("jsonFile");
var databaseFile = Paths.get("database");

Databases.createJsonDatabase(new DatabaseConfiguration(databaseFile));

try (var database = Databases.openJsonDatabase(databaseFile)) {
    database.createResource(ResourceConfiguration.newBuilder("resource").build());

    try (var manager = database.openResourceManager("resource");
         var wtx = manager.beginNodeTrx()) {
        wtx.insertSubtreeAsFirstChild(JsonShredder.createFileReader(pathToJsonFile));
        wtx.commit();
    }
}

まず、データベースを作成します。 次に、データベースを開き、最初のリソースを作成します。 リソースを作成するためのさまざまなオプションがあります(公式ドキュメントを参照)。

次に、リソースで単一の読み取り/書き込みトランザクションを開いて、JSONファイルをインポートします。 トランザクションは、moveToXメソッドをナビゲートするためのカーソルを提供します。 さらに、トランザクションは、ノードを挿入、削除、または変更するためのメソッドを提供します。 XML APIは、リソース内のノードを移動したり、他のXMLリソースからノードをコピーしたりするためのメソッドも提供することに注意してください。

開いた読み取り/書き込みトランザクション、リソースマネージャー、およびデータベースを適切に閉じるために、Javaのtry-with-resourcesステートメントを使用します。

JSONデータでのデータベースとリソースの作成を例示しましたが、XMLデータベースとリソースの作成はほとんど同じです。

次のセクションでは、データベース内のリソースを開き、ナビゲーション軸とメソッドを示します。

6. データベースでリソースを開き、ナビゲートします

6.1. JSONリソースでのナビゲーションの事前注文

ツリー構造をナビゲートするために、コミット後に読み取り/書き込みトランザクションを再利用できます。 ただし、次のコードでは、リソースを再度開き、最新のリビジョンで読み取り専用トランザクションを開始します。

try (var database = Databases.openJsonDatabase(databaseFile);
     var manager = database.openResourceManager("resource");
     var rtx = manager.beginNodeReadOnlyTrx()) {
    
    new DescendantAxis(rtx, IncludeSelf.YES).forEach((unused) -> {
        switch (rtx.getKind()) {
            case OBJECT:
            case ARRAY:
                LOG.info(rtx.getDescendantCount());
                LOG.info(rtx.getChildCount());
                LOG.info(rtx.getHash());
                break;
            case OBJECT_KEY:
                LOG.info(rtx.getName());
                break;
            case STRING_VALUE:
            case BOOLEAN_VALUE:
            case NUMBER_VALUE:
            case NULL_VALUE:
                LOG.info(rtx.getValue());
                break;
            default:
        }
    });
}

子孫軸を使用して、すべてのノードを事前順序(深さ優先)で反復します。 ノードのハッシュは、リソース構成に応じて、デフォルトごとにすべてのノードに対してボトムアップで構築されます。

配列ノードとオブジェクトノードには名前も値もありません。 同じ軸を使用してXMLリソースを反復処理できますが、ノードタイプのみが異なります。

SirixDBは、XMLおよびJSONリソースをナビゲートするための一連の軸、たとえばすべてのXPath-axesを提供します。 さらに、 LevelOrderAxis 、a PostOrderAxis、 a NestedAxis をチェーン軸に提供し、いくつかのConcurrentAxisバリアントを提供してノードを同時に並列にフェッチします。

次のセクションでは、 VisitorDescendantAxis の使用方法を示します。これは、ノードビジターのリターンタイプに基づいて、事前順序で繰り返されます。

6.2. 訪問者の子孫軸

さまざまなノードタイプに基づいて動作を定義することは非常に一般的であるため、SirixDBはビジターパターンを使用します。

VisitorDescendantAxis と呼ばれる特別な軸のビルダー引数としてビジターを指定できます。ノードのタイプごとに、同等のvisitメソッドがあります。 たとえば、オブジェクトキーノードの場合、これはメソッド VisitResult visit(ImmutableObjectKeyNode node)。です。

各メソッドは、タイプVisitResultの値を返します。 VisitResult インターフェイスの唯一の実装は、次の列挙型です。

public enum VisitResultType implements VisitResult {
    SKIPSIBLINGS,
    SKIPSUBTREE,
    CONTINUE,
    TERMINATE
}

VisitorDescendantAxis は、事前にツリー構造を繰り返し処理します。 VisitResultType を使用して、トラバーサルをガイドします。

  • SKIPSIBLINGS は、カーソルが指す現在のノードの正しい兄弟にアクセスせずにトラバーサルを続行する必要があることを意味します
  • SKIPSUBTREE は、このノードの子孫にアクセスせずに続行することを意味します
  • トラバーサルを事前注文で続行する必要がある場合は、CONTINUEを使用します
  • TERMINATE を使用して、トラバーサルをすぐに終了することもできます

Visitor インターフェイスの各メソッドのデフォルトの実装は、ノードタイプごとにVisitResultType.CONTINUEを返します。 したがって、関心のあるノードのメソッドを実装するだけで済みます。 Visitorインターフェイスを実装するMyVisitorというクラスを実装した場合、VisitorDescendantAxisを次のように使用できます。

var axis = VisitorDescendantAxis.newBuilder(rtx)
  .includeSelf()
  .visitor(new MyVisitor())
  .build();

while (axis.hasNext()) axis.next();

MyVisitor のメソッドは、トラバーサルのノードごとに呼び出されます。 パラメータrtxは読み取り専用トランザクションです。 トラバーサルは、カーソルが現在指しているノードから始まります。

6.3. タイムトラベル軸

SirixDBの最も特徴的な機能の1つは、完全なバージョン管理です。 したがって、SirixDBは、1つのリビジョン内でツリー構造を反復処理するためのすべての種類の軸を提供するだけではありません。 次の軸のいずれかを使用して、時間内にナビゲートすることもできます。

  • FirstAxis
  • LastAxis
  • PreviousAxis
  • NextAxis
  • AllTimeAxis
  • FutureAxis
  • PastAxis

コンストラクターは、パラメーターとしてリソースマネージャーとトランザクションカーソルを取ります。  カーソルは、各リビジョンの同じノードに移動します。

軸に別のリビジョン(およびそれぞれのリビジョンのノード)が存在する場合、軸は新しいトランザクションを返します。 戻り値は、それぞれのリビジョンで開かれた読み取り専用トランザクションですが、カーソルは異なるリビジョンの同じノードを指しています。

PastAxisの簡単な例を示します。

var axis = new PastAxis(resourceManager, rtx);
if (axis.hasNext()) {
    var trx = axis.next();
    // Do something with the transactional cursor.
}

6.4. フィルタリング

SirixDBにはいくつかのフィルターが用意されており、これらをFilterAxisと組み合わせて使用できます。 たとえば、次のコードは、オブジェクトノードのすべての子をトラバースし、 {“a”:1、 “b”:”foo”}のようにキー「a」でオブジェクトキーノードをフィルタリングします。

new FilterAxis<JsonNodeReadOnlyTrx>(new ChildAxis(rtx), new JsonNameFilter(rtx, "a"))

FilterAxis は、オプションで、引数として複数のフィルターを取ります。 フィルタは、 JsonNameFilter で、オブジェクトキーの名前をフィルタリングするか、ノードタイプフィルタの1つです: ObjectFilter ObjectRecordFilter ArrayFilter [X183X ]、 StringValueFilter NumberValueFilter BooleanValueFilter 、およびNullValueFilter

この軸は、JSONリソースが「foobar」という名前のオブジェクトキー名でフィルタリングするために次のように使用できます。

var axis = new VisitorDescendantAxis.Builder(rtx).includeSelf().visitor(myVisitor).build();
var filter = new JsonNameFilter(rtx, "foobar");
for (var filterAxis = new FilterAxis<JsonNodeReadOnlyTrx>(axis, filter); filterAxis.hasNext();) {
    filterAxis.next();
}

または、( FilterAxis をまったく使用せずに)軸上で単純にストリーミングしてから、述語でフィルタリングすることもできます。

次の例では、rtxのタイプはNodeReadOnlyTrxです。

var axis = new PostOrderAxis(rtx);
var axisStream = StreamSupport.stream(axis.spliterator(), false);

axisStream.filter((unusedNodeKey) -> new JsonNameFilter(rtx, "a"))
  .forEach((unused) -> /* Do something with the transactional cursor */);

7. データベース内のリソースを変更する

もちろん、リソースを変更できるようにしたいのです。 SirixDBは、コミットごとに新しいコンパクトスナップショットを保存します。

リソースを開いた後、前に見たように、単一の読み取り/書き込みトランザクションを開始する必要があります。

7.1. 簡単な更新操作

変更するノードに移動すると、ノードタイプに応じて、たとえば名前や値を更新できます。

if (wtx.isObjectKey()) wtx.setObjectKeyName("foo");
if (wtx.isStringValue()) wtx.setStringValue("foo");

insertObjectRecordAsFirstChildおよびinsertObjectRecordAsRightSiblingを介して新しいオブジェクトレコードを挿入できます。 すべてのノードタイプに同様のメソッドが存在します。オブジェクトレコードは、オブジェクトキーノードとオブジェクト値ノードの2つのノードで構成されます。

SirixDBは整合性をチェックするため、特定のノードタイプでメソッド呼び出しが許可されていない場合は、チェックされていないSirixUsageExceptionをスローします。

たとえば、キーと値のペアであるオブジェクトレコードは、カーソルがオブジェクトノードにある場合にのみ最初の子として挿入できます。 insertObjectRecordAsX メソッドを使用して、オブジェクトキーノードと他のノードタイプの1つを値として挿入します。

更新メソッドをチェーンすることもできます。この例では、wtxはオブジェクトノードにあります。

wtx.insertObjectRecordAsFirstChild("foo", new StringValue("bar"))
   .moveToParent().trx()
   .insertObjectRecordAsRightSibling("baz", new NullValue());

まず、オブジェクトノードの最初の子として「foo」という名前のオブジェクトキーノードを挿入します。 次に、新しく作成されたオブジェクトレコードノードの最初の子としてStringValueNodeが作成されます。

メソッド呼び出し後、カーソルは値ノードに移動します。 したがって、最初にカーソルをオブジェクトキーノードである親に移動する必要があります。 次に、次のオブジェクトキーノードとその子であるNullValueNodeを右の兄弟として挿入できます。

7.2. 一括挿入

JSONデータをインポートしたときにすでに見たように、より洗練された一括挿入方法も存在します。 SirixDBは、JSONデータを最初の子( insertSubtreeAsFirstChild )および右の兄弟( insertSubtreeAsRightSibling )として挿入するメソッドを提供します。

文字列に基づいて新しいサブツリーを挿入するには、次を使用できます。

var json = "{\"foo\": \"bar\",\"baz\": [0, \"bla\", true, null]}";
wtx.insertSubtreeAsFirstChild(JsonShredder.createStringReader(json));

JSON APIは現在、サブツリーをコピーする可能性を提供していません。 ただし、XMLAPIはそうします。 SirixDBの別のXMLリソースからサブツリーをコピーできます。

wtx.copySubtreeAsRightSibling(rtx);

ここで、読み取り専用トランザクション( rtx )が現在指しているノードは、読み取り/書き込みトランザクション( wtx )が指しているノードの新しい右兄弟としてサブツリーとともにコピーされます。に。

SirixDBは常にメモリ内の変更を適用し、トランザクションのコミット中にそれらをディスクまたはフラッシュドライブにフラッシュします。 唯一の例外は、メモリの制約のために、メモリ内キャッシュが一時ファイルにいくつかのエントリを削除する必要がある場合です。

トランザクションはcommit()または rollback()のいずれかです。 2つのメソッド呼び出しのいずれかの後、トランザクションを再利用できることに注意してください。

SirixDBは、一括挿入を呼び出すときに、内部でいくつかの最適化も適用します。

次のセクションでは、読み取り/書き込みトランザクションを開始する方法に関する他の可能性について説明します。

7.3. 読み取り/書き込みトランザクションを開始します

これまで見てきたように、 commit メソッドを呼び出すことで、読み取り/書き込みトランザクションを開始し、新しいスナップショットを作成できます。 ただし、自動コミットトランザクションカーソルを開始することもできます。

resourceManager.beginNodeTrx(TimeUnit.SECONDS, 30);
resourceManager.beginNodeTrx(1000);
resourceManager.beginNodeTrx(1000, TimeUnit.SECONDS, 30);

30秒ごと、1000回ごとの変更後、または30秒ごとと1000回ごとの変更のいずれかで自動コミットします。

また、読み取り/書き込みトランザクションを開始してから、以前のリビジョンに戻すこともできます。これは、新しいリビジョンとしてコミットできます。

resourceManager.beginNodeTrx().revertTo(2).commit();

その間のすべてのリビジョンは引き続き利用できます。 複数のリビジョンをコミットしたら、正確なリビジョン番号またはタイムスタンプを指定して、特定のリビジョンを開くことができます。

var rtxOpenedByRevisionNumber = resourceManager.beginNodeReadOnlyTrx(2);

var dateTime = LocalDateTime.of(2019, Month.JUNE, 15, 13, 39);
var instant = dateTime.atZone(ZoneId.of("Europe/Berlin")).toInstant();
var rtxOpenedByTimestamp = resourceManager.beginNodeReadOnlyTrx(instant);

8. リビジョンを比較する

リソースの任意の2つのリビジョン間の差異を計算するために、SirixDBに格納されると、diff-algorithmを呼び出すことができます。

DiffFactory.invokeJsonDiff(
  new DiffFactory.Builder(
    resourceManager,
    2,
    1,
    DiffOptimized.HASHED,
    ImmutableSet.of(observer)));

ビルダーへの最初の引数は、すでに数回使用したリソースマネージャーです。 次の2つのパラメーターは、比較するリビジョンです。 4番目のパラメーターは列挙型であり、これを使用して、SirixDBが差分計算を高速化するためにハッシュを考慮する必要があるかどうかを判断します。

SirixDBでの更新操作によってノードが変更された場合、すべての祖先ノードもハッシュ値を適応させます。 2つのリビジョンのハッシュとノードキーが同一である場合、 DiffOptimized.HASHED を指定するとサブツリーに変更がないため、SirixDBは2つのリビジョンのトラバース中にサブツリーをスキップします。

不変のオブザーバーのセットが最後の引数です。 オブザーバーは、次のインターフェースを実装する必要があります。

public interface DiffObserver {
    void diffListener(DiffType diffType, long newNodeKey, long oldNodeKey, DiffDepth depth);
    void diffDone();
}

最初のパラメーターとしてのdiffListenerメソッドは、各リビジョンの2つのノード間で検出されるdiffのタイプを指定します。 次の2つの引数は、2つのリビジョンで比較されたノードの安定した一意のノード識別子です。 最後の引数depthは、SirixDBが比較した2つのノードの深さを指定します。

9. JSONにシリアル化

ある時点で、SirixDBのバイナリエンコーディングでJSONリソースをシリアル化してJSONに戻したいと考えています。

var writer = new StringWriter();
var serializer = new JsonSerializer.Builder(resourceManager, writer).build();
serializer.call();

リビジョン1および2をシリアル化するには:

var serializer = new
JsonSerializer.Builder(resourceManager, writer, 1, 2).build();
serializer.call();

そして、保存されているすべてのリビジョン:

var serializer = new
JsonSerializer.Builder(resourceManager, writer, -1).build();
serializer.call();

10. 結論

低レベルのトランザクションカーソルAPIを使用して、SirixDBのJSONデータベースとリソースを管理する方法を見てきました。 高レベル-APIは複雑さの一部を隠します。

完全なソースコードは、GitHubで入手できます。