Amazon Web Services ブログ

AWS AppSync を使用して Amazon QLDB への GraphQL インターフェイスを構築する: パート 2



この記事は、Amazon Quantum Ledger Database (QLDB)AWS AppSync を統合する方法を説明する 2 回にわたる連載記事の第 2 回です。この組み合わせにより、Amazon QLDB マネージドの台帳データベースに加えて、多目的な GraphQL を利用した API が提供されます。AWS Lambda 関数を作成してクエリを実行することで、Amazon QLDB と AWS AppSync を接続する方法については、「AWS AppSync を使用して Amazon QLDB への GraphQL インターフェイスを構築する: パート 1」をご覧ください。

この記事では統合を引き続き開発し、QLDB で AWS AppSync のより複雑なクエリとデータのミューテーションをサポートします。このチュートリアルでは、Amazon QLDB および AWS AppSync がキャプチャする履歴データのクエリのサポートも行います。

このシリーズのどちらの記事でも、クエリにドライバーの車両情報の DMV データセットを使用しています。GitHub リポジトリでアプリケーション全体を確認することもできます。

次の図は、このプロジェクトのおおまかなアーキテクチャを示しています。AWS AppSync リゾルバーを使用して、Lambda の Amazon QLDB 統合関数で実行するさまざまな操作を実行する PartiQL クエリを作成できます。

この記事のほとんどは、新しい方法で Amazon QLDB とやり取りする新しいリゾルバーの作成に焦点を当てていますが、統合関数を拡張して、単一のトランザクションで複数のクエリをサポートすることもできます。

マルチステップトランザクションのサポートを追加する

Amazon QLDB を使用する場合、後続のクエリを実行する前に、ドキュメント ID などの値を検索する必要があることがよくあります。このシリーズの最初の記事で説明した統合関数のバージョンは、トランザクションごとに 1 つのクエリをサポートしていました。この記事では、1 つのトランザクションで複数のクエリを実行できるように関数を変更します。DMV ユースケースをサポートするために必要なクエリの多くは、次のクエリに渡す前に 1 つのクエリの結果を検査する必要があります。そのため、JMESPath のサポートを追加して、進行中のクエリから値を取得します。JMESPath は、JSON ドキュメントの要素からデータを抽出できる JSON のクエリ言語です。

特定の所有者の車両を検索する必要があるユースケースを想定します。最初に Person テーブルをクエリして、所有者のドキュメント ID (運転免許証番号などの政府の識別子) を取得する必要があります。手順については、「BY 句を使用したドキュメント ID のクエリ」を参照してください。

Vehicle テーブルで車両の詳細を取得する前に、VehicleRegistration テーブルでドライバーの車両を検索することもできます。これらの手順は、単一の JOIN クエリで実行します。詳細については、「結合」を参照してください。

最初のクエリは、BY 句を使用して特定の人物のドキュメント ID を取得します。次のコードを参照してください。

SELECT id FROM Person AS t BY id WHERE t.GovId = ?

そのクエリの結果には、Vehicle テーブルの後続のクエリに必要な ID が含まれています。このクエリでは、PartiQL クエリがネストされたデータとどのように相互作用するかを確認することもできます。この場合、クエリは、Owners フィールドのネストされたフィールドに対して一致します。次のコードを参照してください。

SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?

上記のクエリでは、VehicleRegistration テーブルと Vehicle テーブル全体で JOIN も利用しています。SQL に精通している場合、これは内部結合クエリなので、車両の所有権情報 (VehicleRegistration に格納) から車両のデータ (Vehicle テーブル) に移動できます。

API でこのタイプのトランザクションをサポートするには、統合関数コードを次のコードで調整します。

private String executeTransaction(List<Query> queries) {
  try (QldbSession qldbSession = createQldbSession()) {
    List<String> result = new ArrayList<String>();

    qldbSession.execute((ExecutorNoReturn) txn -> {
      for (Query q : queries) {
        LOGGER.info("Executing query: {}", q.query);
        String lastResult = result.size() > 0 ? result.get(result.size() - 1) : "";
        result.add(executeQuery(txn, q, lastResult));
      }
    }, (retryAttempt) -> LOGGER.info("Retrying due to OCC conflict..."));

    return result.get(result.size() - 1);
  } catch (QldbClientException e) {
    LOGGER.error("Unable to create QLDB session: {}", e.getMessage());
  }

  return "{}";
}

private String executeQuery(TransactionExecutor txn, Query query, String lastResult) {
  final List<IonValue> params = new ArrayList<IonValue>();
  query.getArgs().forEach((a) -> {
    LOGGER.debug("Adding arg {} to query", a);
    try {
      String arg = a.startsWith("$.") && !lastResult.isEmpty() ?
                       queryWithJmesPath(lastResult, a.substring(2)) : a;
      params.add(MAPPER.writeValueAsIonValue(arg));
    } catch (IOException e) {
      LOGGER.error("Could not write value as Ion: {}", a);
    }
  });

  // Execute the query and transform response to JSON string...
  List<String> json = new ArrayList<String>();
  txn.execute(query.getQuery(), params).iterator().forEachRemaining(r -> {
    String j = convertToJson(r.toPrettyString());
    json.add(j);
  });

  return json.toString();
}

これら 2 つの関数を拡張して、1 回の呼び出しで関数への複数のクエリをサポートできるようにします。これにより、より複雑なマルチステップのクエリとミューテーションが可能になります。

次のセクションでは、DMV ユースケースでこの新しい機能を使用する方法について説明します。ただし、最初に JMESPath を使用して結果を検索するためのサポートを追加する必要があります。

進行中の結果からデータを取得するために JMESPath 式を必要とする引数には、「$」を付加します (便利なインジケーターで、それ以上の何者でもなく、式から削除されます)。検索を実行するには、次のコードで新しい関数を導入します。

private String queryWithJmesPath(String json, String jmesExpression)
            throws IOException {
  LOGGER.debug("Query with JMESPath: {} on {}", jmesExpression, json);

  JmesPath<JsonNode> jmespath = new JacksonRuntime();
  Expression<JsonNode> expression = jmespath.compile(jmesExpression);

  ObjectMapper om = new ObjectMapper();
  JsonNode input = om.readTree(json);

  return om.writeValueAsString(expression.search(input)).replaceAll("^\"|\"$", "");
}

上記のコードでは、所有者別に車両を検索するなど、より複雑なクエリを渡すことができます。AWS AppSync パイプラインリゾルバーを使用してマルチステップクエリを実行することもできます。統合関数に追加するのは設計上の決定でした。

次に、このクエリのサポートといくつかのミューテーションを GraphQL API に加え、適切なリゾルバーを実装します。

DMV GraphQL API を拡張する

AWS AppSync では、GraphQL スキーマは、特定の API で使用可能なデータとオペレーションの形状を定義します。前の記事で DMV API のスキーマの作成を開始しましたが、2 番目のクエリを追加して所有者別に車両を検索することで、ここでもスキーマを拡張し続けます。また、Amazon QLDB のデータを変更するオペレーションを提供する新しいタイプのミューテーションを加えます。次のコードを参照してください。

type Query {
  ...
  vehiclesByOwner(govId:ID!):[Vehicle]
}

type Mutation {
  updateVehicleOwner(vin:ID!, govId:ID!):Boolean
  addSecondaryOwner(vin:ID!, govId:ID!):Boolean
}

schema {
  query: Query
  mutation: Mutation
}

vehiclesByOwner」クエリを実行するには、統合関数のデータソースを使用する新しいリゾルバーを次のリクエストマッピングテンプレートでアタッチします。

{
  "version": "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "action": "Query",
    "payload": [
      {
        "query": "SELECT id FROM Person AS t BY id WHERE t.GovId = ?",
        "args": [ "$ctx.args.govId" ]
      },
      {
        "query": "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?",
        "args": [ "$.[0].id" ]
      }
    ]
  }
}

このリゾルバーは前の記事の「getVehicle」リゾルバーに似ていますが、ペイロードに 2 番目のクエリを追加します。これらは、人が所有する車両の検索を可能にする前述のクエリと同じです。最初のクエリは、GraphQL 操作を介して渡された値 (govId) を引数として受け取ります。 2 番目は、JMESPath を使用して、進行中のクエリから値を取得します。レスポンスマッピングテンプレートの詳細については、GitHub リポジトリを参照してください。

次のコードなどの GraphQL ペイロードを使用して、vehiclesByOwner クエリを実行します。

query VehiclesByOwner {
  vehiclesByOwner(govId:"LOGANB486CG") {
    VIN
    Make
    Model
  }
}

次のレスポンスを受け取ります。

{
  "data": {
    "vehiclesByOwner": [
      {
        "VIN": "KM8SRDHF6EU074761",
        "Make": "Tesla",
        "Model": "Model S"
      }
    ]
  }
}

各 PartiQL クエリで選択された値は、リクエストマッピングでハードコードされます。ここでは実装していませんが、統合関数を拡張して、GraphQL 選択セット (上記のクエリの VINMake、および Model) に含まれる値のみを選択できます。必要なデータのみが返されるため、目的の値のみを返すとパフォーマンスが向上する可能性があります。AWS AppSync は、マッピングテンプレートで使用できる GraphQL リクエストの $context.info セクションで選択された値のリストを提供します。詳細については、「AWS AppSync と GraphQL Info Object」を参照してください。

ミューテーション

ミューテーション (データを変更する操作) の実装は、前のクエリとよく似ています。SELECT ステートメントを使用する代わりに、CREATE または UPDATE ステートメントを使用します。次のコードは、updateVehicleOwner ミューテーションのリゾルバーを実装しています。

  "version": "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "action": "Query",
    "payload": [
      {
        "query": "SELECT id FROM Person AS t BY id WHERE t.GovId = ?",
        "args": [ "$ctx.args.govId" ]
      },
      {
        "query": "UPDATE VehicleRegistration AS v SET v.Owners.PrimaryOwner.PersonId = ? WHERE v.VIN = ?",
        "args": [ "$.[0].id", "$ctx.args.vin" ]
      }
    ]
  }
}

このミューテーションは、先のクエリと非常によく似ています。ここでも、渡された govId 値に関連付けられた人物のドキュメント ID を見つけます。2 番目の操作では、UPDATE ステートメントを使用して、VehicleRegistration テーブルのネストされた値を変更します。API はミューテーションの結果を返します (成功した場合は true、それ以外の場合は false)。ただし、更新されたデータを取得して呼び出し元に返すための 3 番目の操作を追加することもできます。

履歴データのクエリ

Amazon QLDB は、ジャーナルと呼ばれる不変のトランザクションログを使用して、データの変更履歴を維持します。この履歴は完全で検証可能です。つまり、ジャーナルの変更をトラバースし返して、データが改ざんされていないことを確認できます。そのためには、各トランザクションの暗号化チェーン署名を検証する必要があります。

DMV のユースケースでは、時間の経過とともに車両の所有権を追跡すると便利です。Amazon QLDB ジャーナルを使用すると、監査テーブルを必要とせずに、このタイプの変更を簡単にクエリできます。車両所有権クエリを追加するには、まずそれを AWS AppSync スキーマ (getOwnershipHistory) に追加します。次のコードを参照してください。

type Query {
  ...
  getOwnershipByHistory(vin:ID!, monthsAgo:Int):[History]
}

getOwnershipHistory クエリは、車両の VIN および検索する月数の 2 つの引数を取ります。クエリは、スキーマに追加する必要がある新しいデータ型である History も返します。次のコードを参照してください。

type History {
  id: ID!
  version: Int!
  txTime: AWSDateTime! # transaction timestamp
  txId: ID!            # transaction ID
  data: HistoryData
  hash: String!
}

union HistoryData = Person | Owner | Vehicle | VehicleRegistration

History タイプには、ユニオンタイプHistoryData を使用する data というフィールドが含まれています。GraphQL のユニオンタイプでは、共通のフィールドを共有しないさまざまなタイプを単一のタイプで返すことができます。DMV API では、HistoryData が API の他のタイプを含めることができることを指定します。

getOwnershipHistory クエリは、前に説明した他のリゾルバーに似ており、同じ統合関数をデータソースとして使用します。このユースケースでは、検索開始時のタイムスタンプも計算します (デフォルトは 3 か月)。次のコードを参照してください。

#set( $months = $util.defaultIfNull($ctx.args.monthsAgo, 3) )
#set( $currEpochTime = $util.time.nowEpochMilliSeconds() )
#set( $fromEpochTime = $currEpochTime - ($months * 30 * 24 * 60 * 60 * 1000) )
#set( $fromTime = $util.time.epochMilliSecondsToISO8601($fromEpochTime) )
{
  "version": "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "action": "Query",
    "payload": [
      {
        "query": "SELECT id FROM VehicleRegistration AS t BY id WHERE t.VIN = ?",
        "args": [ "$ctx.args.vin" ]
      },
      {
        "query": "SELECT * FROM history(VehicleRegistration, `$fromTime`) AS h WHERE h.metadata.id = ?",
        "args": [ "$.[0].id" ]
      }
    ]
  }
}

このクエリは、PartiQL 拡張である history 関数を使用して、テーブルのシステムビューからリビジョンを取得します。history 関数は、Ion タイムスタンプとして示される ISO 8601 形式のオプションの開始時刻と終了時刻を受け入れることができます (必ずバッククォートで囲んでください)。詳細については、「クエリの変更履歴」を参照してください。

Amazon QLDB からの履歴レスポンスには、API が返さないデータが含まれているため、レスポンスマッピングテンプレートは少し複雑です。テンプレートは、応答内の各アイテムを反復処理し、History の形状に一致するオブジェクトを作成します。ユニオンタイプの使用をサポートするには、HistoryData__ typename フィールドも設定する必要があります (このクエリの場合、値は常に「VehicleRegistration」です)。次のコードを参照してください。

#set( $result = $util.parseJson($ctx.result.result) )
#set( $history = [] )

#foreach($item in $result)
  #set( $data = $item.data )
  $util.qr($data.put("__typename", "VehicleRegistration"))
  #set( $h = {
    "id": "$item.metadata.id",
    "version": $item.metadata.version,
    "txTime": "$item.metadata.txTime",
    "txId": "$item.metadata.txId",
    "hash": "$item.hash",
    "data": $data
  } )
  $util.qr($history.add($h))
#end

$util.toJson($history)

特定の車両の所有権の履歴をクエリするには、次の GraphQL クエリを入力します。

query GetVehicleHistory {
  getOwnershipHistory(vin:"3HGGK5G53FM761765") {
    version
    txTime
    data {
      ... on VehicleRegistration {
        Owners {
          PrimaryOwner {
            FirstName
            LastName
          }
        }
      }
    }
  }
}

このクエリを実行する前に、updateVehicleOwner ミューテーションを使用して、この車両のプライマリ所有者を変更してください。これは、ある人が別の人に車を売るのと同じ操作です。次のコードはクエリ結果です。

{
  "data": {
    "getOwnershipHistory": [
      {
        "version": 0,
        "txTime": "2020-01-30T16:11:00.549Z",
        "data": {
          "Owners": {
            "PrimaryOwner": {
              "FirstName": "Alexis",
              "LastName": "Pena"
            }
          }
        }
      },
      {
        "version": 1,
        "txTime": "2020-02-05T22:17:36.262Z",
        "data": {
          "Owners": {
            "PrimaryOwner": {
              "FirstName": "Brent",
              "LastName": "Logan"
            }
          }
        }
      }
    ]
  }
}

その結果、車両の所有者だけでなく、所有者の Alexis が車両を Brent に売却した時期もすばやく見つけることができます。Amazon QLDB はこのデータをジャーナルで透過的にキャプチャし、クエリを容易にします。Amazon QLDB は、改ざんされていないことを確認するためのデータ検証もサポートしていますが、そのプロセスはこの記事では扱いません。

まとめ

Amazon QLDB と AWS AppSync を統合すると、GraphQL API の一部として強力な機能が有効になります。柔軟な統合関数により、単一の Lambda データソースで、Amazon QLDB のデータの履歴を含む多数の PartiQL クエリをサポートできます。詳細と完全なソースコードについては、GitHub リポジトリをご覧ください。

AWS では皆さんのフィードバックをお待ちしています。Amazon QLDB フォーラムにアクセスして、Amazon QLDB の使用方法と他の AWS のサービスとの統合方法を共有してください。

 


著者について

 

Josh Kahn は、アマゾン ウェブ サービスのプリンシパルソリューションアーキテクトです。 AWS のお客様と協力して、データベースプロジェクトに関する指導と技術支援を行い、お客様が AWS を使用する際、ソリューションの価値を向上させられるように手助けしています。