AmplifyでAppSync+DynamoDBテーブルを作成しながらschema.graphqlを読み解く
AmplifyでAppSync+DynamoDBテーブルを作成する
チュートリアルに沿ってAmplifyを作成するところまで実施します。
(ここまで: https://docs.amplify.aws/gen1/react/start/getting-started/data-model/)
本記事は schema.graphql に焦点を当てる記事のため、Amplifyのチュートリアル自体には深く触れません。
amplify add api でAPIを追加しamplify push を実行すると、このようなファイルが自動生成されます。
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
type Blog @model {
id: ID!
name: String!
posts: [Post] @hasMany
}
type Post @model {
id: ID!
title: String!
blog: Blog @belongsTo
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
post: Post @belongsTo
content: String!
}
ここではGraphQLのスキーマを定義していますが、同時にDynamoDBの定義も行っています。DynamoDBを作成するには @model をつける必要があります。
この @ から始まる @xxx はディレクティブと呼ばれます。
Amplifyには多くのディレクティブがあります。
Transformer v1 と v2 で大きくディレクティブが変更になっているのですが、そちらは後述します。
| Directive | Description |
|---|---|
| @model | スキーマ定義に沿って自動でDynamoDBテーブルを作成し、idというフィールドを自動でPrimaryKeyとして追加する(変更可能) またAppSyncのスキーマ定義に createAtとupdateAtフィールドも自動で追加される(この2つの値のデフォルトはReadOnly) |
| @primaryKey |
@model で追加されるidフィールドではなく、独自のフィールドをprimaryKeyとしたい場合に使用する |
| @index | グローバルセカンダリインデックスを作成したい場合に対象のフィールドに付与する |
| @hasOne | 1方向の1対1リレーションシップを設定する |
| @hasMany | 1方向の1対多リレーションシップを設定する |
| @belogsTo | @hasOneまたは@hasManyのフィールドに双方向の1対1リレーションシップを設定する これにより逆引きができるようになる |
| @manyToMany | 2つのテーブルの結合テーブルを設定する |
| @auth | データに対する認可を設定する |
| @function | Lambda関数リゾルバーをフィールドに設定する |
| @http | HTTPリゾルバーをフィールドに設定する |
| @predictions | Amazon Rekognition、Amazon Translate、Amazon PollyなどのAI/MLサービスのオーケストレーションにクエリする |
| @searchable | Amazon OpenSearchへデータを作成する(ストリーミング) |
| @mapsTo | 商用のデータ移行などで一時的にテーブルを使いたいときに使う |
GraphQL Transformer v1 と v2
schema.graphqlの記法にはVersion1とVersion2があります。
記法がv1とv2で大きく異なるので、使用しているTransformerのバージョン確認は必須です。
{
"features": {
"graphqltransformer": {
"addmissingownerfields": true,
"improvepluralization": false,
"validatetypenamereservedwords": true,
"useexperimentalpipelinedtransformer": true,
"enableiterativegsiupdates": true,
"secondarykeyasgsi": true,
"skipoverridemutationinputtypes": true,
"transformerversion": 2, //ここにある
"suppressschemamigrationprompt": true,
.....
}
v1とv2では使用するディレクティブが異なります。
例えばGSIを貼る場合、v1では@keyを使用していましたが、v2では@indexを使用します。
またディレクティブを付与する位置もv1ではtypeの定義と同じ行でしたが、v2からはフィールドに記述するようになりました。
以下の画像ではv1とv2の違いについて示されています。
@indexに加えて@primaryKeyの定義方法も変更されました。

@authディレクティブについても、v1では@authをつけない場合にはデフォルトでAPIキー認証が設定されましたが、v2からはデフォルトでクライアントからのアクセスを拒否する設定になります。

操作許可の部分ではv1とv2で真逆の意味になります。
v1ではoperationsに記載した操作が拒否され、書いていない操作は許可。
v2ではoperationsに記載した操作は許可され、書いていない操作は拒否されます。

v1で使用されていた@connectionディレクティブは、v2から廃止され@hasOne、@hasMany、@belongsTo、@manyToManyに変わりました。
type Post @model {
id: ID!
title: String!
author: User @connection(fields: ["authorID"])
authorID: ID
}
type Post @model {
id: ID!
title: String!
author: User @hasOne(fields: ["authorID"])
authorID: ID
}
引用: https://docs.amplify.aws/gen1/react/tools/cli/migration/transformer-migration/
このようにTransformerのバージョンにより大きく記法が異なります。
そのためAmplifyのドキュメントでは、ドキュメントの上部に以下のような注意書きが出ています。
v1なのかv2なのか確認してからドキュメントを参照するようにしましょう。

リレーションシップに関連するディレクティブを深掘りする
リレーションシップに関連するディレクティブを使用することで、Amplify側でよしなに様々な設定を行なってくれます。
ただ細かく設定したい場合は落とし穴があるため、その点にも触れながらリレーションシップに関連するディレクティブを紹介していきます。
- @hasOne
- @hasMany
- @belongsTo
- @manyToMany
@hasOne
hasOneにすると、対象テーブルのID(Primary Key)がスキーマに追加されることになります。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
}
@hasOneを定義したので、Teamテーブルを参考にProjectテーブルで必要なフィールドを定義します。
mutation CreateProject {
createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {
team {
name
id
}
name
id
}
}
仮にTeamテーブルに2つ以外にteamCodeのようなフィールドがスキーマに定義されていても、Projectテーブルで使用しないのであれば上記と同じMutationで動作します。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
teamCode: String //任意でもう一つフィールドを定義
}
mutation CreateProject {
createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {
team {
name
id
}
name
id
}
}
@hasOneにフィールドを追加することで、該当のteamIdと紐づけることができます。
以下の例ではProjectテーブルのteamIdフィールドとTeamテーブルのIdを紐づけています。
type Project @model {
id: ID!
name: String
teamID: ID
team: Team @hasOne(fields: ["teamID"])
}
type Team @model {
id: ID!
name: String!
}
mutation CreateProject {
createProject(input: { name: "New Project", teamID: "a-team-id" }) {
id
name
team {
id
name
}
}
}
@hasMany
@hasManyディレクティブを付与するには以下のようにします。
このディレクティブを付与すると、リレーションシップを設定した先のテーブル(ここではComment)に対してGSIが自動で作成されます。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
content: String!
}
mutation CreatePost {
createPost(input: { title: "Hello World!!" }) {
title
id
comments {
items {
id
content
}
}
}
}
デフォルトではGSIに対して gsi-{@hasManyディレクティブをつけたオブジェクト型}.{@hasManyディレクティブをつけたフィールド名} という名前が付与されます。
Postのcommentsに対してCommentとのリレーションシップを設定したので、Commentテーブルにgsi-Post.commentsという名前のGSIが作成されています。(ややこしい)
ついでに見ると、デフォルトで作成されるGSIのパーティションキーは
{Postテーブル}{Postテーブルのcommentsフィールド}{Commentテーブルのプライマリーキー}
になってます。

デフォルトではなく独自でインデックスを付与したい場合はディレクティブの後ろに定義することでカスタマイズできます。
また同時にソートキーも設定可能です。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
}
type Comment @model {
id: ID!
postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
content: String!
}
ここでは以下の構成になっています。
-
Postのidは、CommentのpostIDに紐づけられている -
Comment側では、postIDフィールドにbyPostというインデックスが設定されている
この設定により、Post の id をキーとして、Comment テーブルのセカンダリインデックス byPost をクエリすることで、関連する Comment オブジェクトを取得できる仕組みになっています。
実際のコメントテーブルのGSIはこのようになっています。

@authについて
@authディレクティブを使用して、各データに対して細かく認可の設定が可能です。
public, owner, private, groups, custom の選択肢があり、操作よって細かく設定できます。
type Todo
@model
@auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) {
content: String
}
認証方法の種類
| Strategy | Provider | 説明 |
|---|---|---|
| public | apiKey | パブリックデータアクセス。APIキーを使用すれば全てのユーザーにアクセス権が付与される。 |
| public | iam(or identityPool) | 商用環境のパブリックアクセスで推奨される方法。認証されていないユーザーにアクセス許可が付与される。 |
| owner | userPools/oidc | レコードの所有者(owner)のみデータアクセス可能 |
| private | userPools/oidc/iam | サインインしている全てのユーザーにアクセス権が付与される |
| groups | userPools/oidc | 特定のユーザーグループにアクセス権が付与される |
| custom | function | Lambda関数内で独自のカスタム認証ルールを定義し、それを使用する |
privateでproviderを付与しない場合は、Cognitoユーザープールから有効なJWTトークンを持つ全てのユーザーがアクセスできる状態になります。
type Todo @model @auth(rules: [{ allow: private }]) {
content: String
}
これをオーバーライドして、Cognito IDプールの認証済みロールに設定することもできます。
type Post @model @auth(rules: [{ allow: private, provider: iam }]) {
id: ID!
title: String!
}
ちなみにCognito ユーザープールとIDプールの違いは以下のイメージです。
- Cognito ユーザープール: JWTトークンを使用する
- Cognito IDプール: AWSのクレデンシャルを使用する
適用してデプロイしてみる
以下のschema.graphqlファイルの内容でデプロイしてみます。
# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
type Blog @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
posts: [Post] @hasMany
}
type Post @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
title: String!
blog: Blog @belongsTo
comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
createdBy: User @hasOne
}
type Comment @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
content: String!
}
type Team @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
}
type User @model @auth(rule: [{ allow: private, provider: iam}]) {
id: ID!
name: String!
tema: Team @hasOne
}
% amplify push
✔ Successfully pulled backend environment xxxx from the cloud.
Current Environment: xxxx
┌──────────┬────────────────────┬───────────┬───────────────────┐
│ Category │ Resource name │ Operation │ Provider plugin │
├──────────┼────────────────────┼───────────┼───────────────────┤
│ Api │ xxxx │ No Change │ awscloudformation │
└──────────┴────────────────────┴───────────┴───────────────────┘
No changes detected
warningが出ました。このバージョンのAmplify CLIではiam ではなく identityPool を使うように、ということです。
WARNING: Schema is using an @auth directive with deprecated provider 'iam'. Replace 'iam' provider with 'identityPool' provider.
AWSコンソール上でAppSyncのクエリを実際に叩きたい場合は以下の方法で確認できます。
- Amplifyで設定したIAMベースの
@authルールは、Amplifyで作成されたIAMロールでのみ許可されているため、このままでは403エラーになります。
AWSコンソール上でAPIの動作テストをするためには、custom-role.jsonというファイルに設定を記載する必要があります。
{
"adminRoleNames": ["<YOUR_IAM_USER_OR_ROLE_NAME>"]
}
- AppSyncのコンソールから作成されたAPIを選択
- クエリを選択します
- 鍵のアイコンを選択します
- デフォルトはIAM認証になっているので、もし別の認証方法を選択した場合はここで設定してください。
- 設定できたので、試しに
listTeamsを実行してみます
- 結果が表示されます。今回はまだ何もDynamoDBに登録されていないので、何も帰ってきません。

もし認証されていない場合は、以下のようなエラーが返却されます。
@functionについて
@modelディレクティブを付けることで、create、read、update、delete、subscription の各クエリやミューテーションが自動で作成されます。
@functionは自動作成されるクエリとは別に、独自のロジックを持つクエリを手動で定義したい場合に使用します。
使用方法
- カスタムクエリやミューテーションを定義する
type Query {
myCustomQuery(args: String): String
}
type Mutation {
myCustomMutation(args: String): String
}
- リゾルバーオプションを設定する
リゾルバーオプションを設定することで、データ取得や処理の際の挙動を制御できます。
- Lambda関数リゾルバー: Lambda関数を使用してクエリやミューテーションを処理します
- HTTPリゾルバー: クエリやミューテーション時にHTTPエンドポイントを呼び出します
- AppSync JavaScript または VTLリゾルバー: JavaScriptリゾルバーかVTLマッピングテンプレートを使用してロジックをカスタマイズします
Lambda関数リゾルバーの場合
まず、Lambda関数を作成します。
exports.handler = async function (event, context) {
return event.arguments.msg;
};
schema.graphqlのスキーマに@functionを追加します。
type Query {
echo(msg: String): String @function(name: "echofunction")
}
これで設定は完了です。
@function の動作
Amplifyは@functionが定義されたGraphQLの各フィールドに対し、内部的に以下のリソースを作成します。
- AWS IAMロール
- Lambda関数を呼びだす権限を持つ
- AWS AppSyncに対する信頼ポリシー
- AWS AppSyncデータソース
- AWS AppSync パイプライン関数
- AWS AppSync リゾルバー
認可ルール(@auth)の追加
カスタムリゾルバーの定義にも、@authをつけることで認可ルールを追加することができます。
※VTLリゾルバーを使用している場合には併用不可
type Mutation {
myCustomMutation(args: String): String
@auth(rules: [{ allow: private, provider: iam }])
}
おわり
Amplifyは公式ドキュメントを確認しながら進めることで、効率的にバックエンド構築が可能です。しかし、リレーションシップや認可設定など細かい部分では注意点も多く、実際に試行錯誤することが重要です。
よく読めばドキュメントにあったり、なかったりするので検証しながら進めるのが吉だと感じました。
参考にさせていただいた記事
Discussion