AWS AppSync の Data API での Aurora PostgreSQL の使用
AWS AppSync は、Data API で有効化されている Amazon Aurora クラスターに対して SQL ステートメントを実行するためのデータソースを提供します。AWS AppSync リゾルバーで GraphQL クエリ、ミューテーション、サブスクリプションを使用して、Data API に対して SQL ステートメントを実行できます。
注記
このチュートリアルでは、US-EAST-1
リージョンを使用しています。
クラスターの作成
Amazon RDS データソースを AWS AppSync に追加する前に、まず Aurora Serverless クラスターでData API を有効にします。また、AWS Secrets Manager を使用してシークレットを設定する必要があります。Aurora Serverless クラスターを作成するには、AWS CLI を使用できます。
aws rds create-db-cluster \ --db-cluster-identifier appsync-tutorial \ --engine aurora-postgresql --engine-version 13.11 \ --engine-mode serverless \ --master-username USERNAME \ --master-user-password COMPLEX_PASSWORD
これにより、クラスターの ARN が返されます。コマンドでクラスターのステータスを確認できます。
aws rds describe-db-clusters \ --db-cluster-identifier appsync-tutorial \ --query "DBClusters[0].Status"
AWS Secrets Manager コンソールからシークレットを作成します。あるいは、以下のような入力ファイルで前の手順の USERNAME
と COMPLEX_PASSWORD
を使用して、AWS CLI からシークレットを作成します。
{ "username": "USERNAME", "password": "COMPLEX_PASSWORD" }
このシークレットを CLI にパラメータとして渡します。
aws secretsmanager create-secret \ --name appsync-tutorial-rds-secret \ --secret-string file://creds.json
これにより、シークレットの ARN が返されます。Aurora Serverless クラスターとシークレットの ARN をメモしておいてください。これらの ARN は後でデータソースを作成するときに AWS AppSync コンソールで使用します。
Data API の有効化
クラスターのステータスが available
に変わったら、「Amazon RDS のドキュメント」に従ってData API を有効にします。Data API は AWS AppSync データソースとして追加する前に有効にする必要があります。AWS CLI を使用して Data API を有効にすることもできます。
aws rds modify-db-cluster \ --db-cluster-identifier appsync-tutorial \ --enable-http-endpoint \ --apply-immediately
データベースとテーブルの作成
Data API を有効にしたら、動作することを AWS CLI で aws rds-data
execute-statement
コマンドを使用して確認します。これにより、Aurora Serverless クラスターが正しく設定されていることを AWS AppSync API への追加前に確認できます。まず、--sql
パラメータで TESTDB データベースを作成します。
aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --sql "create DATABASE \"testdb\""
これがエラーなしで実行されたら、create table
コマンドを使用して 2 つのテーブルを追加します。
aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --database "testdb" \ --sql 'create table public.todos (id serial constraint todos_pk primary key, description text not null, due date not null, "createdAt" timestamp default now());' aws rds-data execute-statement \ --resource-arn "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial" \ --secret-arn "arn:aws:secretsmanager:us-east-1:123456789012:secret:appsync-tutorial-rds-secret" \ --database "testdb" \ --sql 'create table public.tasks (id serial constraint tasks_pk primary key, description varchar, "todoId" integer not null constraint tasks_todos_id_fk references public.todos);'
すべてが問題なく実行されたら、API のデータソースとしてクラスターを追加できます。
GraphQL スキーマを作成する
設定されたテーブルで Aurora Serverless の Data API が実行されたので、GraphQL スキーマを作成します。これは手動で行うこともできますが、AWS AppSync では、API 作成ウィザードを使用して既存のデータベースからテーブル設定をインポートすることですぐに開始できます。
開始方法
-
AWS AppSync コンソールで [API の作成]、[Amazon Aurora クラスターから始める] の順に選択します。
-
[API 名] などの API の詳細を指定し、API を生成するデータベースを選択します。
-
データベースを選択します。必要に応じてリージョンを更新し、Aurora クラスターと TESTDB データベースを選択します。
-
シークレットを選択し、[インポート] を選択します。
-
テーブルが検出されたら、型名を更新します。
Todos
をTodo
に変更し、Tasks
をTask
に変更します。 -
[スキーマをプレビュー] を選択して、生成されたスキーマをプレビューします。スキーマは以下のようになります。
type Todo { id: Int! description: String! due: AWSDate! createdAt: String } type Task { id: Int! todoId: Int! description: String }
-
ロールについては、AWS AppSync により新しいロールを作成するか、以下のようなポリシーを持つロールを作成できます。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "rds-data:ExecuteStatement", ], "Resource": [ "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial", "arn:aws:rds:us-east-1:123456789012:cluster:appsync-tutorial:*" ] }, { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue" ], "Resource": [ "arn:aws:secretsmanager:us-east-1:123456789012:secret:your:secret:arn:appsync-tutorial-rds-secret", "arn:aws:secretsmanager:us-east-1:123456789012:secret:your:secret:arn:appsync-tutorial-rds-secret:*" ] } ] }
このポリシーには、ロールにアクセス許可を付与するステートメントが 2 つあることに注意してください。最初のリソースは Aurora クラスターで、2 番目のリソースは AWS Secrets Manager ARN です。
[次へ] を選択し、設定の詳細を確認して [API の作成] を選択します。これで完全に動作する API が作成されました。API の全詳細は、[スキーマ] ページで確認できます。
RDS のリゾルバー
API 作成フローでは、型とやり取りするリゾルバーが自動的に作成されました。[スキーマ] ページを確認すると、以下のことを行うために必要なリゾルバーが見つかるでしょう。
-
Mutation.createTodo
フィールドでtodo
を作成する。 -
Mutation.updateTodo
フィールドでtodo
を更新する。 -
Mutation.deleteTodo
フィールドでtodo
を削除する。 -
Query.getTodo
フィールドで 単一のtodo
を取得する。 -
Query.listTodos
フィールドでtodos
をすべて一覧表示する。
Task
型にアタッチされた類似のフィールドとリゾルバーがあります。いくつかのリゾルバーを詳しく見ていきましょう。
Mutation.createTodo
AWS AppSync コンソールのスキーマエディタの右側で、createTodo(...): Todo
の横にある testdb
を選択します。リゾルバーコードは、rds
モジュールの insert
関数を使用して、todos
テーブルにデータを追加する挿入ステートメントを動的に作成します。ここでは Postgres を使用しているため、挿入されたデータをreturning
ステートメントを活用して返すことができます。
due
フィールドの DATE
型を適切に指定するようにリゾルバーを更新しましょう。
import { util } from '@aws-appsync/utils'; import { insert, createPgStatement, toJsonObject, typeHint } from '@aws-appsync/utils/rds'; export function request(ctx) { const { input } = ctx.args; // if a due date is provided, cast is as `DATE` if (input.due) { input.due = typeHint.DATE(input.due) } const insertStatement = insert({ table: 'todos', values: input, returning: '*', }); return createPgStatement(insertStatement) } export function response(ctx) { const { error, result } = ctx; if (error) { return util.appendError( error.message, error.type, result ) } return toJsonObject(result)[0][0] }
リゾルバーを保存します。型ヒントは、入力オブジェクト内の due
をDATE
型として適切にマークします。これにより、Postgres エンジンが値を適切に解釈できるようになります。次に、スキーマを更新して CreateTodo
入力から id
を削除します。Postgres データベースは生成された ID を返すことができるため、これを使って作成し、単一のリクエストとして結果を返すことができます。
input CreateTodoInput { due: AWSDate! createdAt: String description: String! }
変更を加え、スキーマを更新します。[クエリ] エディタに移動して、データベースに項目を追加します。
mutation CreateTodo { createTodo(input: {description: "Hello World!", due: "2023-12-31"}) { id due description createdAt } }
次のような結果が得られます。
{ "data": { "createTodo": { "id": 1, "due": "2023-12-31", "description": "Hello World!", "createdAt": "2023-11-14 20:47:11.875428" } } }
Query.listTodos
コンソールのスキーマエディタの右側で、listTodos(id: ID!): Todo
の横にある testdb
を選択します。リクエストハンドラーは select ユーティリティ関数を使用して、実行時にリクエストを動的に構築します。
export function request(ctx) { const { filter = {}, limit = 100, nextToken } = ctx.args; const offset = nextToken ? +util.base64Decode(nextToken) : 0; const statement = select({ table: 'todos', columns: '*', limit, offset, where: filter, }); return createPgStatement(statement) }
due
の日付に基づいて todos
をフィルタリングしたいとします。due
の値を DATE
にキャストするようにリゾルバーを更新しましょう。インポートのリストとリクエストハンドラを更新します。
import { util } from '@aws-appsync/utils'; import * as rds from '@aws-appsync/utils/rds'; export function request(ctx) { const { filter: where = {}, limit = 100, nextToken } = ctx.args; const offset = nextToken ? +util.base64Decode(nextToken) : 0; // if `due` is used in a filter, CAST the values to DATE. if (where.due) { Object.entries(where.due).forEach(([k, v]) => { if (k === 'between') { where.due[k] = v.map((d) => rds.typeHint.DATE(d)); } else { where.due[k] = rds.typeHint.DATE(v); } }); } const statement = rds.select({ table: 'todos', columns: '*', limit, offset, where, }); return rds.createPgStatement(statement); } export function response(ctx) { const { args: { limit = 100, nextToken }, error, result, } = ctx; if (error) { return util.appendError(error.message, error.type, result); } const offset = nextToken ? +util.base64Decode(nextToken) : 0; const items = rds.toJsonObject(result)[0]; const endOfResults = items?.length < limit; const token = endOfResults ? null : util.base64Encode(`${offset + limit}`); return { items, nextToken: token }; }
クエリを試してみましょう。[クエリ] エディタで、以下を実行します。
query LIST { listTodos(limit: 10, filter: {due: {between: ["2021-01-01", "2025-01-02"]}}) { items { id due description } } }
Mutation.updateTodo
Todo
を update
することも可能です。[クエリ] エディタから、id
1
の最初の Todo
項目を更新しましょう。
mutation UPDATE { updateTodo(input: {id: 1, description: "edits"}) { description due id } }
更新する項目の id
を指定する必要があることに注意してください。条件を指定して、特定の条件を満たす項目のみを更新することもできます。例えば、記述が edits
で始まる場合にのみ項目を編集したい場合があります。
mutation UPDATE { updateTodo(input: {id: 1, description: "edits: make a change"}, condition: {description: {beginsWith: "edits"}}) { description due id } }
create
オペレーションと list
オペレーションを処理したのと同じように、リゾルバーを更新して due
フィールドを DATE
にキャストできます。これらの変更を updateTodo
に保存します。
import { util } from '@aws-appsync/utils'; import * as rds from '@aws-appsync/utils/rds'; export function request(ctx) { const { input: { id, ...values }, condition = {}, } = ctx.args; const where = { ...condition, id: { eq: id } }; // if `due` is used in a condition, CAST the values to DATE. if (condition.due) { Object.entries(condition.due).forEach(([k, v]) => { if (k === 'between') { condition.due[k] = v.map((d) => rds.typeHint.DATE(d)); } else { condition.due[k] = rds.typeHint.DATE(v); } }); } // if a due date is provided, cast is as `DATE` if (values.due) { values.due = rds.typeHint.DATE(values.due); } const updateStatement = rds.update({ table: 'todos', values, where, returning: '*', }); return rds.createPgStatement(updateStatement); } export function response(ctx) { const { error, result } = ctx; if (error) { return util.appendError(error.message, error.type, result); } return rds.toJsonObject(result)[0][0]; }
次に、以下の条件で更新を試してください。
mutation UPDATE { updateTodo( input: { id: 1, description: "edits: make a change", due: "2023-12-12"}, condition: { description: {beginsWith: "edits"}, due: {ge: "2023-11-08"}}) { description due id } }
Mutation.deleteTodo
deleteTodo
ミューテーションで Todo
を delete
できます。これは updateTodo
ミューテーションと同様に動作し、削除する項目の id
を指定する必要があります。
mutation DELETE { deleteTodo(input: {id: 1}) { description due id } }
カスタムクエリの記述
rds
モジュールユーティリティを使用して SQL ステートメントを作成しました。独自でカスタムした静的ステートメントを記述して、データベースとやり取りすることもできます。まず、スキーマを更新して CreateTask
入力から id
フィールドを削除します。
input CreateTaskInput { todoId: Int! description: String }
次に、いくつかのタスクを作成します。タスクには Todo
との外部キーの関係があります。
mutation TASKS { a: createTask(input: {todoId: 2, description: "my first sub task"}) { id } b:createTask(input: {todoId: 2, description: "another sub task"}) { id } c: createTask(input: {todoId: 2, description: "a final sub task"}) { id } }
Query
型に getTodoAndTasks
という新しいフィールドを作成します。
getTodoAndTasks(id: Int!): Todo
Todo
型に tasks
フィールドを追加します。
type Todo { due: AWSDate! id: Int! createdAt: String description: String! tasks:TaskConnection }
スキーマを保存します。コンソールのスキーマエディタの右側で、getTodosAndTasks(id:
Int!): Todo
に対して [リゾルバーをアタッチ] を選択します。Amazon RDS データソースを選択します。以下のコードでリゾルバーを更新します。
import { sql, createPgStatement,toJsonObject } from '@aws-appsync/utils/rds'; export function request(ctx) { return createPgStatement( sql`SELECT * from todos where id = ${ctx.args.id}`, sql`SELECT * from tasks where "todoId" = ${ctx.args.id}`); } export function response(ctx) { const result = toJsonObject(ctx.result); const todo = result[0][0]; if (!todo) { return null; } todo.tasks = { items: result[1] }; return todo; }
このコードでは、sql
タグテンプレートを使用して、実行時に動的な値を安全に渡すことができる SQL ステートメントを記述します。createPgStatement
は、一度に最大 2 つの SQL リクエストを受け入れることができます。これを利用して、todo
に対して 1 つのクエリを送信し、tasks
に対して別のクエリを送信します。これは、JOIN
ステートメントやその他の方法を使用して行うこともできます。つまり、独自の SQL ステートメントを記述してビジネスロジックを実装できるということです。[クエリ] エディタでクエリを使用するには、次のことを試します。
query TodoAndTasks { getTodosAndTasks(id: 2) { id due description tasks { items { id description } } } }
クラスターの削除
重要
クラスターは完全に削除されます。このアクションを実行する前に、プロジェクトを徹底的に確認してください。
クラスターを削除するには
$ aws rds delete-db-cluster \ --db-cluster-identifier appsync-tutorial \ --skip-final-snapshot