SirixDBのガイド

*1. 概要+

*
このチュートリアルでは、https://sirix.io [SirixDB]の概要とその最も重要な設計目標について説明します。
次に、低レベルのカーソルベースのトランザクションAPIについて説明します。

2. SirixDBの機能

SirixDBは、進化データを保存するログ構造の一時的なlink:/eclipse-jnosql[NoSQL]ドキュメントストアです。 ディスク上のデータを上書きすることはありません。 したがって、データベース内のリソースの完全な改訂履歴を効率的に復元およびクエリすることができます。 SirixDBは、新しいリビジョンごとに最小のストレージオーバーヘッドが作成されるようにします。
現在、SirixDBは2つの組み込みのネイティブデータモデル、つまりバイナリXMLストアとJSONストアを提供しています。

* 2.1。 設計目標*

最も重要な基本原則と設計目標の一部は次のとおりです。
  • 同時実行-SirixDBにはほとんどロックが含まれておらず、
    可能な限りマルチスレッドシステムに適しています

  • *非同期REST API – *操作は独立して実行できます。各
    トランザクションは特定のリビジョンにバインドされており、リソース上の1つの読み取り/書き込みトランザクションのみがN個の読み取り専用トランザクションに同時に許可されます

  • *バージョン管理/改訂履歴– * SirixDBは次の改訂履歴を保存します。
    ストレージのオーバーヘッドを最小限に抑えながら、データベース内のすべてのリソース。 読み取りおよび書き込みのパフォーマンスは調整可能です。 これは、リソースの作成時に指定できるバージョン管理タイプに依存します

  • *データの整合性– * ZFSのようなSirixDBは、
    親ページのページ。 これは、SirixDBの開発者が将来データベースをパーティション分割および複製することを目的としているため、将来の読み取り時にほとんどすべてのデータ破損が検出できることを意味します

  • * Copy-on-writeセマンティクス-*ファイルシステムBtrfsおよび
    ZFS、SirixDBはCoWセマンティクスを使用します。つまり、SirixDBはデータを上書きしません。 代わりに、データベースページのフラグメントがコピーされ、新しい場所に書き込まれます

  • 改訂ごとおよびレコードごとのバージョン管理- SirixDBは、
    ページごとのバージョンですが、レコードごとのバージョンもあります。 したがって、データページ内のレコードの潜在的に小さな部分を変更するたびに、ページ全体をコピーしてディスクまたはフラッシュドライブの新しい場所に書き込む必要はありません。 代わりに、データベースリソースの作成中に、バックアップシステムまたはスライディングスナップショットアルゴリズムから既知のいくつかのバージョン管理戦略の1つを指定できます。 指定するバージョン管理タイプは、SirixDBがデータページのバージョン管理に使用します

  • *保証された原子性(WALなし)-*システムは決して入力されません
    一貫性のない状態(ハードウェア障害がない限り)。これは、予期しない電源オフがシステムに損傷を与えないことを意味します。 これは、write-ahead-log(https://en.wikipedia.org/wiki/Write-ahead_logging[WAL])のオーバーヘッドなしで達成されます。

  • *ログ構造化されたSSDフレンドリー– SirixDBは書き込みと同期をバッチ処理します
    コミット中にすべてがフラッシュドライブに順次送信されます。 コミットされたデータを上書きすることはありません

    今後の記事でフォーカスをより高いレベルに切り替える前に、まずJSONデータで例示された低レベルAPIを紹介します。 たとえば、XMLデータベースとJSONデータベースの両方を照会するためのXQuery-APIまたは非同期の一時的なRESTful API。 基本的に、XMLリソースの保存、トラバース、比較にも微妙な違いがある同じ低レベルAPIを使用できます。
    SirixDBを使用するには、少なくともlink:/java-11-string-api[Java 11]を使用する必要があります。*

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

例に従うには、まずhttps://search.maven.org/search?q=g:io.sirix%20AND%20a:sirix-coreを含める必要があります
<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_エンコーディングによって他のノードを参照します。
link:/uploads/encoding-100x48.png%20100w []
図の番号は、単純な連続番号ジェネレーターで生成された自動生成された一意の安定したノードIDです。
すべてのノードには、最初の子、左兄弟、右兄弟、および親ノードがあります。 さらに、SirixDBは、各ノードの子の数、子孫の数、およびハッシュを格納できます。
次のセクションでは、SirixDBのコア低レベルJSON APIを紹介します。

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();
    }
}
最初にデータベースを作成します。 次に、データベースを開き、最初のリソースを作成します。 リソースを作成するためのさまざまなオプションがあります(https://sirix.io/documentation.html [公式ドキュメントを参照])。
次に、*リソースで*単一の読み取り/書き込みトランザクション*を開いてJSONファイルをインポートします。 トランザクションは、_moveToX_メソッドを介したナビゲーション用のカーソルを提供します。 さらに、トランザクションは、ノードを挿入、削除、または変更するメソッドを提供します。 XML APIは、リソース内のノードを移動したり、他のXMLリソースからノードをコピーしたりする*メソッドも提供することに注意してください。
開いた読み取り/書き込みトランザクション、リソースマネージャー、およびデータベースを適切に閉じるには、Javaのlink:/java-try-with-resources[_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 _、_ PostOrderAxis _、_ NestedAxis_を軸にチェーンし、複数の_ConcurrentAxis_バリアントを提供して、ノードを並行して並行してフェッチします。
次のセクションでは、_VisitorDescendantAxis_を使用する方法を示します。_VisitorDescendantAxis_は、ノードビジターの戻り値の型に基づいて、事前順序で反復します。

6.2. 訪問者の子孫軸

さまざまなノードタイプに基づいて動作を定義することは非常に一般的であるため、SirixDBはlink:/java-visitor-pattern [訪問者パターン]を使用します。
_VisitorDescendantAxis._ calledという特別な軸のビルダー引数としてビジターを指定できます。ノードのタイプごとに、同等のvisit-methodがあります。 たとえば、オブジェクトキーノードの場合は、_VisitResult visit(ImmutableObjectKeyNode node)._メソッドです。
各メソッドは、_VisitResult_型の値を返します。 _VisitResult_インターフェイスの唯一の実装は、次の列挙型です。
public enum VisitResultType implements VisitResult {
    SKIPSIBLINGS,
    SKIPSUBTREE,
    CONTINUE,
    TERMINATE
}
_VisitorDescendantAxis_は、ツリー構造を事前順序で繰り返します。 __VisitResultType__sを使用して、走査をガイドします。
  • _SKIPSIBLINGS_は、トラバーサルをせずに続行することを意味します
    カーソルが指す現在のノードの右の兄弟を訪問する

  • _SKIPSUBTREE_は、の子孫を訪問せずに続行することを意味します
    このノード

  • 走査が先行予約で続行される場合は、_CONTINUE_を使用します

  • _TERMINATE_を使用して、走査をすぐに終了することもできます

    _Visitor_インターフェイスの各メソッドのデフォルトの実装は、各ノードタイプに対して_VisitResultType.CONTINUE_を返します。 したがって、興味のあるノードのメソッドを実装するだけです。 _MyVisitor_という_Visitor_インターフェースを実装するクラスを実装した場合、次の方法で_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_と組み合わせて使用​​できます。 たとえば、次のコードは、オブジェクトノードのすべての子をトラバースし、_ \ {aa:1、bのようにキー「a」でオブジェクトキーノードをフィルタリングします。 「foo」€_。
new FilterAxis<JsonNodeReadOnlyTrx>(new ChildAxis(rtx), new JsonNameFilter(rtx, "a"))
_FilterAxis_は、オプションで引数として複数のフィルターを取ります。 フィルターは、オブジェクトキー内の名前をフィルター処理する_JsonNameFilter_、または_ObjectFilter _、_ ObjectRecordFilter _、_ ArrayFilter _、_ 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は現在、サブツリーをコピーする可能性を提供していません。 ただし、XML APIはサポートします。 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);
1000秒ごとの変更後、または30秒ごとおよび1000回ごとの変更ごとに、30秒ごとに自動コミットします。
また、読み取り/書き込みトランザクションを開始してから、以前のリビジョンに戻すことができます。これを新しいリビジョンとしてコミットできます。
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. リビジョンの比較

SirixDBに保存されたリソースの2つのリビジョン間の差異を計算するには、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を使用して、JSONデータベースとSirixDBのリソースを管理する方法を見てきました。 高レベルのAPIは、複雑さの一部を隠します。
完全なソースコードは、https://github.com/eugenp/tutorials/tree/master/persistence-modules/sirix [GitHub]で入手できます。