AWS Amplify フレームワークの使い方Part18〜GraphQL Transform v2 @hasOne/@hasMany/@belongsTo/@manyToMany (旧@ connection)編〜

# はじめに
非常に複雑だった@connectionがv2になってシンプルになって帰ってきました。
今回の変更で、よりAmplifyを始めやすくなったのではないでしょうか。

# 環境
＠aws-amplify/cli 7.6.3

# 定義
今回、公式ドキュメントに定義としては記載がなかったので、ドキュメントを見て分かる範囲で記載しています。

```
directive @hasOne(fields: [String!])
directive @hasMany(indexName: String, fields: [String!])
directive @belongsTo(fields: [String!])
directive @manyToMany(relationName: String)
```
## fields
fieldsを指定することで、指定したキーを使って紐付先のテーブルから情報を取得することができます。

## indexName
`@index`で設定したセカンダリインデックスを使って、データを取得することができます。indexNameがない場合は、紐付先のプライマリーキーでのデータ取得になります。。

## relationName
manyToManyで自動生成されるテーブル名を指定します。

# スキーマへの記載方法
以下のようにまとめていきます。

- 1対1
- 1対多
- 多対多

## 1対1
### fields指定なし / 双方向データ取得なし
 
```graphql:graghql/schema.graphql
type Project @model {
  id: ID!
  name: String
  team: Team @hasOne
  # projectTeamId: ID! が自動生成
}

type Team @model {
  id: ID!
  name: String!
}
```
```javascript:graghql/mutations.js
mutation CreateProject {
  createProject(input: {projectTeamId: "team-id", name: "Some Name"}) {
    id
    projectTeamId
    name
    team {
      name
      id
    }
  }
}
```

紐付け元に、${紐付元のテーブル名}${紐付先のテーブル名}Idが自動生成されます。
今回は、`projectTeamId`という変数が自動生成され、create時に`projectTeamId`を保存することで、紐付けが完了します。

### fields指定あり / 双方向データ取得なし
`projectTeamId`の変数名を自分で指定したい場合は、以下のように記載します。
個人的には、fields指定ありの方が一手間はかかりますが、スキーマファイル内で明示的に記載できるので、後々わかりやすい気がしています。

```graphql:graghql/schema.graphql
type Project @model {
  id: ID!
  teamId: ID!
  name: String
  team: Team @hasOne(fields: ["teamId"])
}

type Team @model {
  id: ID!
  name: String!
}
```
```javascript:graghql/mutations.js
mutation CreateProject {
  createProject(input: { name: "New Project", teamId: "a-team-id"}) {
    id
    teamId
    name
    team {
      id
      name
    }
  }
}
```

### fields指定なし / 双方向データ取得あり
fields指定なしで双方向からデータの取得がしたい場合は、以下のように記載します。

```graphql:graghql/schema.graphql
type Project @model {
  id: ID!
  name: String
  team: Team @hasOne
  # projectTeamId : ID! が@hasOneによって自動生成
}

type Team @model {
  id: ID!
  name: String!
  project: Project @belongsTo
  # teamProjectId : ID! が@belongsToによって自動生成
}
```
```javascript:graghql/mutations.js
mutation CreateProject {
  createProject(input: { name: "New Project", projectTeamId: "a-team-id"}) {
    id
    projectTeamId
    name
    team {
      id
      teamProjectId
      name
      project {
        id
        projectTeamId
        name
      }
    }
  }
}
```
### fields指定あり / 双方向データ取得あり
fields指定なしで双方向からデータの取得がしたい場合は、以下のように記載します。

```graphql:graghql/schema.graphql
type Project @model {
  id: ID!
  teamId: ID!
  name: String
  team: Team @hasOne(fields: ["teamId"])
}

type Team @model {
  id: ID!
  projectId: ID!
  name: String!
  project: Project @belongsTo(fields: ["projectId"])
}
```
```javascript:graghql/mutations.js
mutation CreateProject {
  createProject(input: { name: "New Project", teamId: "a-team-id"}) {
    id
    teamId
    name
    team {
      id
      projectId
      name
      project {
        id
        teamId
        name
      }
    }
  }
}
```

## 1対多
### fields指定なし / 双方向データ取得なし

```graphql:graghql/schema.graphql
type Post @model {
  id: ID!
  title: String!
  comments: [Comment] @hasMany
}

type Comment @model {
  id: ID!
  content: String!
  # postCommentsId: ID! が自動生成
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    comments {
      items {
        id
        postCommentsId
        content
      }
    }
  }
}
```

hasManyの紐付け先テーブル内に、`${紐付け元のテーブル名}${@hasManyをつけた項目名}Id`が自動生成されます。

上記の例では、commentを新規作成する場合は、必ずinputにpostCommentsIdを保存する必要があります。

### fields指定あり / 双方向データ取得なし
紐付け用のIDを自動生成したくないときは、hasOneと同様にfiledを指定＋indexNameを指定、@indexを利用することで変更できます。

```graphql:graghql/schema.graphql
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!
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello world!"}) {
    id
    title
    comments {
      items {
        postId
        content
        id
      }
    }
  }
}
```
### fields指定なし / 双方向データ取得あり
これまでの流れだと、hasManyとbelongsToによって、`postCommentsId`と`commentPostId`の2つの変数が作成されそうですが、`commentPostId`だけ生成されます。

```graphql:graghql/schema.graphql
type Post @model {
  id: ID!
  title: String!
  comments: [Comment] @hasMany
}

type Comment @model {
  id: ID!
  content: String!
  post: Post @belongsTo
  # postCommentsId: ID がhasManyによって自動生成
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    comments {
      items {
        id
        commentPostId
        content
        post {
          id
          title
        }
      }
    }
  }
}
```

### fields指定あり / 双方向データ取得あり
こちらもお好みで、変数を指定したい場合は、fieldsを指定します。

```graphql:graghql/schema.graphql
type Post @model {
  id: ID!
  title: String!
  comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
}

type Comment @model {
  id: ID!
  postId: ID! @index(name: "byPost")
  content: String!
  post: Post @belongsTo(fields: ["postId"])
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    comments {
      items {
        id
        postId
        content
        post {
          id
          title
        }
      }
    }
  }
}
```

## 多対多

### @manyToManyを利用した場合
中間テーブルの作成を勝手にやってくれます。


```graphql:graghql/schema.graphql
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")
}
# 実質的に以下をスキーマに記載した時と同じ動作になります。
# type PostTags @model {
#   id: ID!
#   postID: ID!
#   tagID: ID!
#   post: Post @belongsTo(fields: ["postID"])
#   tag: Tag @belongsTo(fields: ["tagID"])
# }
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    content
    tags {
      items {
        id
        postID
        tagID
        tag {
          id
          label
          posts {
            items {
              post {
                id
                title
                content
              }
            }
          }
        }
        post {
          id
          title
          content
        }
      }
    }
  }
}
```
### @manyToManyを利用しない場合/双方データ取得あり
現状、manyToManyで生成される中間テーブルに独自の項目を追加したり、アクセス権限を縛ったりはできません。(厳密にはやり方わからない。)
なので、とりあえずmanyToManyを手動でスキーマに書くと以下のようになります。

```graphql:graghql/schema.graphql
type Post @model {
  id: ID!
  title: String!
  content: String
  tags: [PostTags] @hasMany(indexName: "byPost", fields: ["id"])
}

type Tag @model {
  id: ID!
  label: String!
  posts: [PostTags] @hasMany(indexName: "byTag", fields: ["id"])
}

type PostTags @model {
  id: ID!
  postId: ID! @index(name: "byPost")
  tagId: ID!  @index(name: "byTag")
  post: Post @belongsTo(fields: ["postId"])
  tag: Tag @belongsTo(fields: ["tagId"])
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    content
    tags {
      items {
        id
        postId
        tagId
        tag {
          id
          label
          posts {
            items {
              post {
                id
                title
                content
              }
            }
          }
        }
        post {
          id
          title
          content
        }
      }
    }
  }
}
```

### @manyToManyを利用しない場合/双方データ取得なし
地味に多対多で双方向データ取得不要なケースもあるので、それも記載しておきます。

```graphql:graghql/schema.graphql
# 例：Postからのみ取得できればOKパターン
type Post @model {
  id: ID!
  title: String!
  content: String
  tags: [PostTags] @hasMany(indexName: "byPost", fields: ["id"])
}

type Tag @model {
  id: ID!
  label: String!
}

type PostTags @model {
  id: ID!
  postId: ID! @index(name: "byPost")
  tagId: ID!
  tag: Tag @hasOne(fields: ["tagId"])
}
```
```javascript:graghql/mutations.js
mutation CreatePost {
  createPost(input: {title: "Hello World!!"}) {
    id
    title
    content
    tags {
      items {
        id
        postId
        tagId
        tag {
          id
          label
        }
      }
    }
  }
}
```


# 注意点/メモ

## fieldsに指定できるのはstringのみ？
未検証ですが、`@connection`の時は、fieldsに指定できるのはstringのみでしたので、おそらく今回もその可能性が高いと思っています。

その場合は、自分でカスタムリゾルバーを作成しましょう。

https://docs.amplify.aws/cli/graphql-transformer/resolvers/

## IdとID問題
v7.6.3では、自動生成されるidは、hasOne/hasMany/belongToは`xxxId`、manyToManyで生成されるテーブルのみ`xxxID`で生成されます。なぜこうなっているのかは、わかりませんが、注意が必要です。

## indexName
未検証なので定義のところには記載しませんでしたが、hasOneやbelongToでも多分indexNameでindex指定できると思われます。また検証したら更新します。

# おわりに
その昔issueで議論されていた内容が、気づいたら新機能として実装されていました。
これもAmplifyの開発は活発である証拠とかと思いますので、引き続き、今後のAmplifyの進化に期待です！

# 参考
https://docs.amplify.aws/cli/graphql/overview/