Amazon Web Services ブログ

AWS AppSync と RDS Data API を使って Amazon Aurora MySQL データベース用の GraphQL API を構築する方法

AWS AppSync が、Data API で構成された Amazon Auroraクラスタ上で稼働している既存の MySQL や PostgreSQL データベースのテーブルに基づいて、GraphQL API を簡単に作成できるようになりました。既存のデータベース用の API を構築する場合、開発者は通常、テーブルを正確に表現するインターフェースを構築しなければなりませんが、これには時間がかかり、エラーが発生しやすいプロセスです。AppSync は、データベースを検出し、それに一致する GraphQL 型を生成できる新しいイントロスペクション機能によってこの問題を解決します。AppSync コンソールでは、この新機能を使用して、コードを記述することなく、わずか数ステップでデータベースからすぐに使用できる GraphQL API を生成できます。さらに、Amazon Relational Database Service (RDS) 用の JavaScript リゾルバにも改良が加えられており、新しい SQL タグ付きテンプレートと SQL ヘルパー関数により、リゾルバで SQL ステートメントを簡単に記述できるようになっています。

本記事では、API を即座に構築するために AWS コンソールでこの機能を使い始める方法を紹介し、JavaScript リゾルバで新しい RDS ユーティリティライブラリを使用する方法を紹介します。

注:このブログの機能は、RDS Data API をサポートする Amazon RDSデータベースを使用しています。Data API をサポートしていない MySQLや PostgreSQL データベースに接続するには、「既存の MySQL と PostgreSQL データベース用の GraphQL API の作成」を参照してください。

AppSync コンソールでの設定

AppSync の新しいイントロスペクション機能は、Data API で構成された Amazon Aurora クラスターで実行されている既存のデータベースで使用できます。例えば、以下のテーブルが定義された MySQL データベースがあり、API を提供したいとします。

CREATE TABLE conversations (  
id INT NOT NULL PRIMARY KEY,  
name VARCHAR(255) NOT NULL,  
created_at DATETIME DEFAULT CURRENT_TIMESTAMP  
);  
  
CREATE TABLE messages (  
id VARCHAR(36) PRIMARY KEY,  
conversation_id INT NOT NULL,  
sub VARCHAR(36) NOT NULL,  
body TEXT NOT NULL,  
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  
FOREIGN KEY (conversation_id) REFERENCES conversations(id)  
);  
  
CREATE TABLE conversation_participants (  
conversation_id INT NOT NULL,  
sub varchar(36) NOT NULL,  
last_read_at DATETIME,  
PRIMARY KEY (conversation_id, sub),  
FOREIGN KEY (conversation_id) REFERENCES conversations(id)  
);

AWS AppSync コンソールで、Create API をクリックし、GraphQL API で Start with an Amazon Aurora cluster を選択します。Next をクリックし、API に名前を付け、次の画面でデータベース情報を入力してイントロスペクションを開始します。

Aurora クラスタに Data API を設定し、データベースユーザーの認証情報を AWS Secrets Manager に保存しておく必要があります。このユーザーには、データベース、スキーマ、テーブルの設定を読み取る権限が必要です。設定の詳細については、Data API ドキュメントを参照してください。Secrets Manager のシークレットに対して GetSecretValue、クラスタに対して ExecuteStatement を実行する権限が必要です。また、自分のリソースでアクセスするために AppSync の権限を付与する必要があります。コンソールでロールを作成することもできますし、自分で用意することもできます。Import をクリックして、イントロスペクション処理を開始します。完了すると、検出されたテーブルは以下のように表示されます。

デフォルトでは、型名はテーブル名と同じですが、カスタマイズすることもできます。生成されるスキーマからテーブルを除外することもできます。以下のように型名を変更してください。

  • Conversation_ParticipantsParticipant
  • ConversationsConversation
  • MessagesMessage

Next をクリックし、Create queries, mutations, and subscriptions for all models を選択します。Next をクリックし、変更内容を確認して、Create API をクリックします。スキーマの作成を開始し、リゾルバをフィールドにアタッチします。これで、クエリエディタからデータベースとやり取りしたり、GraphQL API に接続するクライアントアプリケーションを構築したりできるようになります。API を使用するには、クエリエディタに向かいます。左側のメニューから Queries を選択します。まずは新しい会話 (Conversation) を作成しましょう。

mutation CreateConvo {
  createConversation(input: {id: 1, name: "stand up meeting"}) {
    created_at
    id
    name
  }
}

これでデータベースに会話が追加されます。次にメッセージを追加しましょう。

mutation CreateMsg {
  createMessage(input: {body: "Hello world! Things are looking good.", conversation_id: 1, id: "new-message", sub: "johndoe"}) {
    body
    conversation_id
    created_at
    id
    sub
  }
}

AppSync のリアルタイム機能はすぐに使用できます。例えば、会話で新しいメッセージを聞くには、新しいタブまたはウィンドウでクエリエディタを開き、フォローサブスクリプションを入力します。

subscription OnCreate {
  onCreateMessage(conversation_id: 1) {
    body
    conversation_id
    created_at
    id
    sub
  }
}

元の Queries タブで、別のメッセージを送信します。

mutation CreateMsg {
  createMessage(input: {body: "Hello again. Nothing to report", conversation_id: 1, id: "2nd-message", sub: "johndoe"}) {
    body
    conversation_id
    created_at
    id
    sub
  }
}

2 つめのクエリエディタにサブスクリプションがトリガーされたことが表示されます。

自動生成されたリゾルバを編集し、必要に応じてカスタマイズすることができます。例えば、新しいメッセージが作成されるたびに API が ID を自動生成するように変更してみます。createMessage リゾルバを以下のように更新します。

import { util } from '@aws-appsync/utils';
import { insert, select, createMySQLStatement, toJsonObject } from '@aws-appsync/utils/rds';

export function request(ctx) {
    const { input: values } = ctx.args;
    values.id = util.autoUlid() // <<< set the ULID 
    const doInsert = insert({ table: 'messages', values });
    const doSelect = select({
        table: 'messages',
        columns: '*',
        where: { id: { eq: values.id } },
        limit: 1,
    });
    return createMySQLStatement(doInsert, doSelect);
}

export function response(ctx) {
    const { error, result } = ctx;
    if (error) {
        return util.appendError(error.message, error.type, result);
    }
    return toJsonObject(result)[1][0];
}

上のコードでは、2 つのリクエストをデータベースに送っています。1 つめは、指定された ULID (Universally Unique Lexicographically Sortable Identifier) で新しいメッセージを作成します。2 つめは挿入された行をフェッチしてデータベースからデータを返します。これは、MySQL を使用して作成されたばかりの行 (自動生成されたカラムと値を含む) を取得するときに便利なパターンです。レスポンスから 2つめのオブジェクト (インデックス1) を取得します。これは、私が送信した 2 つめのステートメント (doSelect) の結果に対応します。

次に、スキーマの CreateMessageInput Input を更新して、id フィールドを削除します。

input CreateMessageInput {
    # id: String! # << comment out or remove
    conversation_id: Int!
    sub: String!
    body: String!
    created_at: String
}

新しいメッセージを送信します。

query ListMsgs {
  listMessages(filter: {conversation_id: {eq: 1}, created_at: {ge: "2023-11-13", lt: "2023-11-13 22:23"}, sub: {ne: "john"}}) {
    items {
      id
      created_at
      body
      sub
    }
  }
}

生成されたIDでレスポンスが返ってきます。

{
  "data": {
    "createMessage": {
      "body": "up and up",
      "conversation_id": 1,
      "created_at": "2023-11-13 23:24:06",
      "id": "01HF5FZNM3M9PEYC1234567890",
      "sub": "john"
    }
  }
}

いくつかのメッセージを追加したところで、会話メッセージをすべて選択してみましょう。例えば、ここでは created_at タイムスタンプと sub 値でフィルタリングしています。

query ListMsgs {
  listMessages(filter: {conversation_id: {eq: 1}, created_at: {ge: "2023-11-13", lt: "2023-11-13 22:23"}, sub: {ne: "john"}}) {
    items {
      id
      created_at
      body
      sub
    }
  }
}

RDS の新しいユーティリティ関数の使用

RDS の新しいユーティリティを使用して、データベーステーブルを操作することができます。Conversation 型を変更して、participants フィールドを追加してみましょう。このフィールドは、最近読まれたメッセージ (last_read_at) に基づいて、最近アクティブになった会話参加者の ID を返します。

type Conversation {
    id: Int!
    name: String!
    created_at: String
    participants: [String]
}

次に、@aws-appsync/utils/rds が提供する select ユーティリティ関数を使ってカスタム select 文を書くために、getConversation リゾルバを更新します。

import { util } from '@aws-appsync/utils';
import {
  select,
  createMySQLStatement,
  toJsonObject,
} from '@aws-appsync/utils/rds';

export function request(ctx) {
  const { id } = ctx.args;
  const doSelect = select({
    table: 'conversations',
    columns: '*',
    where: { id: { eq: id } };,
    limit: 1,
  });
  const doGetLatest = select({
    table: 'conversation_participants',
    columns: ['sub'],
    where: { conversation_id: { eq: id } },
    orderBy: [{ column: 'last_read_at', dir: 'DESC' }],
    limit: 10,
  });
  return createMySQLStatement(doSelect, doGetLatest);
}

export function response(ctx) {
  const { error, result } = ctx;
  if (error) {
    return util.appendError(error.message, error.type, result);
  }
  const res = toJsonObject(result);
  const convo = res[0][0];
  if (convo) {
    convo.participants = (res[1] ?? []).map((p) => p.sub);
  }
  return convo;
}

リゾルバでは、最大 2 つのステートメントをデータベースに送信することができるので、1 回の実行で会話 (Conversation) とその参加者を取得することができます。createMySQLStatement 関数は、MySQL ステートメントを適切にエスケープし、引用符で囲むリクエストを作成します。変更してみましょう。クエリエディタでクエリを実行します。

query get {
  getConversation(id: 1) {
    id
    participants
  }
}

以下の結果が返ってきます。

{
  "data": {
    "getConversation": {
      "id": 1,
      "participants": [
        "John",
        "Sarah"
      ]
    }
  }
}

カスタム SQL 文の作成

新しい SQL タグ付きテンプレートを使って独自の SQL 文を書くことができます。タグ付きテンプレートを使うと、テンプレート式を通して実行時に動的な値を受け入れる静的な SQL 文を書くことができます。会話要約クエリをスキーマに追加し、新しい Summary 型を追加してみましょう。

type Query {
    getConversationSummary(id: Int!, since: AWSDate!): Summary
}

type Summary {
    id: Int!
    total_messages: Int
    total_words: Int
    total_participants: Int
}

次に、getConversationSummary にリゾルバをアタッチします。

import {
  sql,
  createMySQLStatement,
  toJsonObject,
  typeHint,
} from '@aws-appsync/utils/rds';

export function request(ctx) {
  const query = sql`
SELECT
    c.id AS id,
    COUNT(DISTINCT m.id) AS total_messages,
    COUNT(DISTINCT cp.sub) AS total_participants,
    SUM(LENGTH(m.body) - LENGTH(REPLACE(m.body, ' ', '')) + 1) AS total_words
FROM
    conversations c
LEFT JOIN
    messages m ON c.id = m.conversation_id
LEFT JOIN
    conversation_participants cp ON c.id = cp.conversation_id
WHERE
    c.id = ${ctx.args.id}
    AND m.created_at >= ${typeHint.DATE(ctx.args.since)}
GROUP BY
    c.id, c.name;
`;
  return createMySQLStatement(query);
}

export function response(ctx) {
  return toJsonObject(ctx.result)[0][0];
}

ここでは、sql タグ付きテンプレートを使って SQL 文を書いています。SQL タグ付きテンプレートを使うと、式によって動的な値のみを受け付ける静的なステートメントを書くことができます。式を通して渡された値は、プレースホルダを通して自動的にデータベースエンジンに送られます。また、since 引数がデータベースエンジンによって DATE 型として扱われることを示すために、型ヒントを使用しています。

変更を保存し、クエリを実行します。

query get {
  getConversationSummary(id: 1, since: "2023-01-01") {
    id
    total_messages
    total_participants
    total_words
  }
}

以下の結果が返ってきます。

{
  "data": {
    "getConversationSummary": {
      "id": 1,
      "total_messages": 9,
      "total_participants": 2,
      "total_words": 66
    }
  }
}

まとめ

Aurora クラスターで稼働している既存の MySQL データベースから AppSync GraphQL API を作成する手順を紹介しました。この記事では MySQL に焦点を当てましたが、PostgreSQL データベースでも同じことができます。ご自身のデータベースで始めるには、AppSync ドキュメントで Data API による RDS イントロスペクションの詳細をご覧ください。RDS 用の JavaScript リゾルバの新しいユーティリティの詳細については、JavaScript リゾルバのリファレンスを参照してください。ガイド付きの導入については、Aurora PostgreSQL with Data API のチュートリアルを参照してください。独自の JavaScript リゾルバを書き始めるには、@aws-appsync/utils パッケージの最新版をダウンロードまたは更新してください。

本記事は「Build a GraphQL API for your Amazon Aurora MySQL database using AWS AppSync and the RDS Data API」を翻訳したものです。

翻訳者について

稲田 大陸

AWS Japan で働く筋トレが趣味のソリューションアーキテクト。普段は製造業のお客様を中心に技術支援を行っています。好きな AWS サービスは Amazon Location Service と AWS Amplify で、日本のお客様向けに Amazon Location Service の解説ブログなどを執筆しています。