Amplifyで簡単にDB設計をしよう!~リレーション定義の巻~
前回は一番基盤となるテーブル定義について解説しました。
今回はよりDB設計の表現の幅を広げるリレーションの設定について解説していきます!テーブル間のリレーション設定
Amplifyでは、@modelタイプ間で"1対1"、1対多"、"所属関係"、"多対多"といったリレーションを簡単に作成できます。
表にまとめてみました!
リレーション | ディレクティブ | 説明 |
---|---|---|
一対一 | @hasOne |
二つのモデル間で一方向の一対一の関係を作成します。例:プロジェクトは一つのチームを"持っている"。 |
一対多 | @hasMany |
二つのモデル間で一方向の一対多の関係を作成します。例:投稿は多くのコメントを"持っている"。 |
所属関係 | @belongsTo |
"has one"や"has many"の関係を双方向にするための関係。例:プロジェクトは一つのチームを"持っている"と同時に、チームはプロジェクトに"所属している"。 |
多対多 | @manyToMany |
二つのモデル間で多対多の関係を実現するための"結合テーブル"を設定。例:ブログは多くのタグを"持っている"と同時に、タグも多くのブログを"持っている"。 |
1対1の設定(@hasOne)
@hasOneディレクティブを使用して、二つのモデル間で一方向の一対一の関係を作成しましょう。
例として、以下のようにプロジェクトはチームを"持っている"関係を考えてみましょう。
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
}
Amplifyはこの定義を元に、関連するアイテムをテーブルから取得するためのクエリやミューテーションを自動的に生成してくれます!
以下のミューテーション定義と実行によって、新しいプロジェクトを作成できます。
mutation CreateProject {
createProject(input: {projectTeamId: "team-id", name: "Some Name"}) {
team {
name
id
}
name
id
}
}
このミューテーションでは、"projectTeamId"というフィールドがデフォルトで定義されています。
import { createProject } from './graphql/mutations';
const params = {
input: { projectTeamId: 'team-id', name: 'Some Name' },
};
const result = await API.graphql(graphqlOperation(createProject, params));
const project = result.data.createProject;
これで、特定のチームIDとプロジェクト名を使って、新しいプロジェクトを作成し、その情報を取得できます!
カスタマイズ
リレーション情報を保存するフィールドをカスタマイズしたい場合は、fields配列引数を設定して、それをタイプのフィールドにマッチさせることができるんですよ✨
例として、以下のようにプロジェクトにteamIDフィールドを追加して、チームの識別子として使用します。
type Project @model {
id: ID!
name: String
teamID: ID
team: Team @hasOne(fields: ["teamID"])
}
type Team @model {
id: ID!
name: String!
}
この場合、@hasOneはteamIDを使って、関連するTeamオブジェクトを取得することができます。
以下のミューテーションを使って、新しいプロジェクトを作成してみましょう!
mutation CreateProject {
createProject(input: { name: "New Project", teamID: "a-team-id"}) {
id
name
team {
id
name
}
}
}
この
そして、以下のコードを使って、GraphQLミューテーションを実行します:
import { createProject } from './graphql/mutations';
const params = {
input: { teamID: 'team-id', name: 'New Project' },
};
const result = await API.graphql(graphqlOperation(createProject, params));
const project = result.data.createProject;
ちなみに、@hasOneリレーションは、関連するモデルのプライマリキーを参照しています。デフォルトではidを使用しますが、@primaryKeyディレクティブで上書きすることもできるんですよ!🌟
1対多の設定(@hasMany)
さて、@hasMany
ディレクティブを使って、2つのモデル間で一方向の1対多の関係を作成しましょう!
type Post @model {
id: ID!
title: String!
comments: [Comment] @hasMany
}
type Comment @model {
id: ID!
content: String!
}
これにより、関連するComment
レコードを元のPost
レコードから取得するためのクエリやミューテーションが生成されます。
mutation CreatePost {
createPost(input: {title: "Hello World!!"}) {
title
id
comments {
items {
id
content
}
}
}
}
そして、以下のコードを使って、GraphQLミューテーションを実行します:
import { createPost } from './graphql/mutations';
const params = {
input: { title: 'Hello World!!' },
};
const result = await API.graphql(graphqlOperation(createPost, params));
const post = result.data.createPost;
const comments = post.comments.items;
裏側では、@hasMany
は関連するテーブル上にセカンダリインデックスを作ってくれています。これで、あるテーブルから関連するテーブルをクエリできるようにしています。
1対多関係のための特定のセカンダリインデックスをカスタマイズすしたい場合、関連するテーブルのフィールドに@index
ディレクティブを作成して、セカンダリインデックスを割り当てれば大丈夫です。
@indexでは、インデックス名とsort keyを設定することができます。
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!
}
この場合、Comment
タイプにはPost
レコードの参照を保存するためのpostID
フィールドが追加されています。@hasMany
は、このpostID
を使って、Comment
テーブルのセカンダリインデックス"byPost"をクエリして、関連するComment
オブジェクトを取得できます。
mutation CreatePost {
createPost(input: {title: "Hello world!"}) {
comments {
items {
postID
content
id
}
}
title
id
}
}
そして、以下のコードを使って、GraphQLミューテーションを実行します:
import { createPost, createComment } from './graphql/mutations';
import { getPost } from './graphql/mutations';
// create post
const postParams = {
input: { title: 'Hello World!!' },
};
const result = await API.graphql(graphqlOperation(createPost, postParams));
const post = result.data.createPost;
// create comment
const commentParams = {
input: { content: 'Hi!', postID: post.id },
};
await API.graphql(graphqlOperation(createComment, commentParams));
// get post
const result = await API.graphql(graphqlOperation(getPost, { id: post.id }));
const postWithComments = result.data.createPost;
const postComments = postWithComments.comments.items; // access comments
このコードは、特定のタイトルで投稿を作成し、その投稿にコメントを追加できます。
所属関係の設定(@belongsTo)
「@belongsTo」ディレクティブを使うと、「has one」や「has many」の関係を双方向にすることができます!
type Project @model {
id: ID!
name: String
team: Team @hasOne
}
type Team @model {
id: ID!
name: String!
project: Project @belongsTo
}
これにより、Project
からTeam
を、そして逆にTeam
からProject
をクエリできるようになります!
mutation CreateProject {
createProject(input: { name: "New Project", teamID: "a-team-id"}) {
id
name
team { # ProjectからTeamをクエリ
id
name
project { # そして、TeamからProjectをクエリ
id
name
}
}
}
}
import { createProject, createTeam, updateTeam } from './graphql/mutations';
// Teamを作成
const teamParams = {
input: { name: 'New Team' },
};
const result = await API.graphql(graphqlOperation(createTeam, teamParams));
const team = result.data.createTeam;
// Projectを作成
const projectParams = {
input: { name: 'New Project', projectTeamId: team.id },
};
const result = await API.graphql(graphqlOperation(createProject, projectParams));
const project = result.data.createProject;
// Teamを更新
const updateParams = {
input: { id: team.id, teamProjectId: project.id },
};
const updateTeamResult = await API.graphql(graphqlOperation(updateTeam, updateParams));
ちなみに、@belongsTo
はfields引数なしで使うこともできます。その場合、親の主キーを参照するフィールドが自動的に生成されます!
もちろん、カスタムフィールドを設定して親オブジェクトの参照を保存することもできます。以下はその例です。
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: Post @belongsTo(fields: ["postID"])
}
多対多の設定(@manyToMany)
「@manyToMany」ディレクティブを使うと、2つのモデル間で多対多の関係を作成できるんです!両方のモデルに共通のrelationName
を提供して、多対多の関係を結ぶ魔法のような繋がりを作りましょう!
type Post @model {
id: ID!
title: String!
content: String
tags: [Tag] @manyToMany(relationName: "PostTags")
}
type Tag @model {
id: ID!
label: String!
posts: [Post] @manyToMany(relationName: "PostTags")
}
裏側では、@manyToMany
ディレクティブがrelationName
として名付けられた結合テーブルを作成して、多対多の関係をサポートしています!
mutation CreatePost {
createPost(input: {title: "Hello World!!"}) {
id
title
content
tags { # "結合テーブル" PostTagsをクエリ
items {
tag { # Postから関連するTagレコード
id
label
posts { # "結合テーブル" PostTagsを再度クエリ
items {
post { # Tagから関連するPostレコード
id
title
content
}
}
}
}
}
}
}
}
import { createPost, createTag, createPostTags } from './graphql/mutations';
import { listPosts } from './graphql/queries';
// Postを作成
const postParams = {
input: { title: 'Hello World' },
};
const result = await API.graphql(graphqlOperation(createPost, postParams));
const post = result.data.createPost;
// Tagを作成
const tagParams = {
input: { label: 'My Tag' },
};
const tagResult = await API.graphql(graphqlOperation(createTag, tagParams));
const tag = tagResult.data.createTag;
// PostとTagを繋げる
const postTagParams = {
input: { postId: post.id, tagId: tag.id },
};
await API.graphql(graphqlOperation(createPostTags, postTagParams));
// Postを取得
const listPostsResult = await API.graphql(graphqlOperation(listPosts));
const posts = listPostsResult.data.listPosts;
const postTags = posts[0].tags; // PostからTagを取得
まとめ
結構作ってみると明示的でないフィールドを気を遣って作ってくれているのがAmplifyという印象ですかね。
それが良い時もあるが、見えないところが潜在的な実装漏れなどを引き起こしそうという懸念もありますね。
Discussion