この記事は AWS AmplifyとAWS×フロントエンド Advent Calendar 2022 22日目の記事です。
AWS Amplify は主にプロトタイプやMVPを開発する際、作りたいと思ったものをクイックに実装できる便利なサービスです。フロントエンドはAmplify Hostingを利用することでホスティング作業が非常に簡単になりました。また、amplify-jsライブラリを利用すれば、AWS SDKのみを利用するよりも、効率よくAWSの各サービスに連携することが可能になっています。Figmaとの連携など、Amplify Studioを活用する場面もあるかと思います。
一方、バックエンドについても、GraphQLモードの場合、AppSyncとDynamoDBを利用したオススメの構成を自動生成してくれます。上手く使えばとても役立ちますが、従来まではデータモデリングを含め実現方法のコツを試行錯誤しながら覚える必要がありました。
そんな中、昨年末GraphQL Transformer V2が発表され、特にデータモデル間のリレーション定義が簡潔に書けるようになりました。基本的な部分さえ押さえれば、実践的に活用または応用することが以前よりやりやすくなりました。
この記事では、新しいデータモデリングのリレーション定義にフォーカスして、公式ドキュメントに基づき新しく追加された4つのディレクティブ( @hasOne
, @hasMany
, @belongsTo
, @manyToMany
)の基本的な使い方について解説します。合わせて、自動生成される仕組みのイメージができるように、DynamoDBの使われ方についても紹介していきます。
@hasOne – 2つのモデル間に一方向の 1:1 リレーションを作成
まず、あるプロジェクトとそのプロジェクトを推進するチームがあるとします。また、プロジェクトの担当チームを取得するクエリーを書きたいと想定します。ここで、例えば「各プロジェクトを担当するチームは1つのみ」という仕様がある場合、以下のようにスキーマを定義します。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
}
amplify push でデプロイしてレコードを登録してみると、以下のようにDynamoDBがセットされます。
Project テーブル
@hasOne
ディレクティブは、Teamのレコードを引くための ID ( proejctTeamId
) がスキーマに自動追加されます。この項目に担当TeamのID( team_1
)を登録することで、ProjectからTeamのレコードが取得できるようになります。
Team テーブル
「プロジェクトの担当チームを取得する」クエリーのサンプル
このように、 @hasOne
ディレクティブは 1:1 の関係にあるレコードのリレーションを定義し、解決するリゾルバーを生成してくれます。
@hasMany – 2つのモデル間に、一方向の 1:N リレーションを作成
ここでは、ブログの記事( Post
)と、その記事についているコメント欄( Comment
)があるとします。また、記事とその記事に投稿されているコメント一覧を一気に取得するクエリーを書くと想定します。「一つの記事」に対して「複数のコメント」がある構成を表現する場合、以下のようにスキーマを定義します。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
content: String!
}
amplify push でデプロイしてレコードを登録してみると、以下のようにDynamoDBがセットされます。
Post テーブル
Comment テーブル
@hasMany
の場合、Many(多)になる側(=Commentテーブル)にマッピング項目が追加され、以下のようにGSI-PKインデックスが自動生成された状態になります。
これにより、以下のようなクエリーを書くことができるようになります。postCommentsId
に指定されているPostのIDがGSI-PKとして使われます。
belongsTo – 2つのモデルの間に、双方向で 1:1 または 1:N のリレーションを作成
簡単にいうと @hasOne
または @hasMany
で指定したモデルを、逆方向でも取得できるようにするものです。
例えば、@hasOne
で指定したProjectとTeamの関係では、「Project」を指定して「Team」を取得する形でした。もし「Team」から「Project」を取得するクエリーを書きたくなった場合、以下のようにスキーマを定義します。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
# project フィールドを追加、@belongsTo を指定することでTeamからもProjectを引けるようになる
project: Project @belongsTo
}
amplify push でデプロイしてTeamのレコードにProjectを登録してみます。
Team テーブル
teamProjectId
という項目がスキーマに自動追加されますので、ここにProjectのIDを指定することで、@hasOne
パターンとは逆の方向からProjectを取得できるようになります。
以下、クエリーのサンプルです。
このように、Teamから紐付いているProjectのレコードを引けるようになります。
※マッピング用の項目名( “projectTeamId” など)が紛らわしくて気になる方は、公式ドキュメントにて任意のキー名を指定する方法がありますので是非ご参照ください。
続いて、@hasMany
の場合、PostからComment一覧を取得する形でした。もし、Commentを指定してそのコメントが紐付いているPostを見つけたくなった場合、以下のようにスキーマを定義します。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
content: String!
# project フィールドを追加、@belongsTo を指定することでCommentからもPostを引けるようになる
post: Post @belongsTo
}
これを amplify push でデプロイすると、以下のようにpostが引けるようになります。スキーマとリゾルバが自動追加され、DynamoDBの構成には変更ありません。
@manyToMany – 2つのモデルの N:N リレーションを作成
2つのモデルを Joint する対応が必要になるケースです。例えば、記事( Post
)にタグ付け( Tag
)を行うとし、以下のような状態を考えてみます。
- タグは「タグA」「タグB」「タグC 」の3種類がある
- 「記事A」には「タグA」と「タグB」が指定されている
- 「記事B」には「タグB」と「タグC」が指定されている
この状態で、以下2つのユースケースがあると想定します。
- 「記事A」に指定されているタグの一覧を取得する
- 「タグB」が指定されている記事の一覧を取得する
これらを実現するためのスキーマは以下のように定義します。
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
# tags フィールドを追加、relationNameを指定(Jointテーブルの名前になります)
tags: [Tag] @manyToMany(relationName: "PostTags")
}
type Tag @model {
id: ID!
label: String!
# tags フィールドを作成、relationNameを指定(Jointテーブルの名前になります)
posts: [Post] @manyToMany(relationName: "PostTags")
}
amplify push でデプロイすると、以下のように Joint に対応するための中間テーブルが作成され、PostとTagのマッピングを登録します。GraphQL Transformer V1ではこの中間テーブルを自前で定義する必要がありましたが、V2では中間テーブルを自動作成してくれるようになりました。
PostTags テーブル
PostとTagの両方からマッピングの一覧を取得できるようにするために、それぞれのGSIが自動作成されます。
Postに紐付いているタグの一覧を取得する場合、以下のようにクエリーを書くことができます。
同じく、Tagに紐付いているPostの一覧を取得する場合、以下のようにクエリーを書くことができます。
注意点としては、中間テーブルに独自の項目を追加することはできないので、もし付帯情報の追加など中間テーブルをカスタマイズしたくなった場合は @hasMany
<> @belongsTo
パターンを利用して「PostTags」テーブルを自前で定義する必要があります。
type Post @model {
id: ID!
title: String!
content: String
tags: [PostTags] @hasMany
}
type Tag @model {
id: ID!
label: String!
posts: [PostTags] @hasMany
}
type PostTags @model {
id: ID!
post: Post @belongsTo
tag: Tag @belongsTo
# ...
}