AWS Amplify フレームワークの使い方Part7〜GraphQL Transform @key編〜

# 2021/12/12更新
気づいたら`@key`(AmplifyCLIのv6かv7から？)がより直感的でわかりやすくなっていました！！！

https://qiita.com/too/items/37ea2a2b87268fbba4e8

# はじめに
今回は、この子の登場でかゆいところに手が届くようになった`@key`をご紹介していきます。
この記事は、ある程度DynamoDBの基礎知識がないとそもそもわけがわからないと思いますので、基礎知識がない方は、まず以下の参考記事に目を通してもらうといいと思います。

[DynamoDBのキー・インデックスについてまとめてみた](https://qiita.com/shibataka000/items/e3f3792201d6fcc397fd)

# 定義
`@key`の定義について解説していきます。初見の方は、ここは飛ばしてあとから読むことをおすすめします。

```
directive @key(fields: [String!]!, name: String, queryField: String) on OBJECT
```
## fields
ここで、パーテンションキーやソートキーを指定します。

## name
nameを指定しない場合は、プライマリーキーの設定扱いになります。nameを設定することで、セカンダリインデックスが簡単に指定できます。

## queryField
セカンダリインデックスを設定した際に、このqueryFieldを設定することで、セカンダリインデックスを使ったクエリでの検索が可能になります。

# 使い方
使い方はいたってシンプルです。

## プライマリーキーを自動設定
```:schema.graphql
// ①
type Customer @model {
    id: ID!
    email: String!
    username: String
}

// ②
type Customer @model @key(field: ["id"]){
    id: ID!
    email: String!
    username: String
}
```
①と②は同じです。IDの場合だけ自動でプライマリーキーになるのか、一番に宣言したカラムが上でプライマリーキーになるのかは検証していませんが、プライマリーキーをIDにする際は、基本的に`@key`を使用する必要はありません。

## プライマリーキーにID以外を設定
```:schema.graphql
type Customer @model @key(fields: ["email"]) {
    id: ID!
    email: String!
    username: String
}
```
こんな感じで、fieldsにemailを設定することで、emailがプライマリーキーに設定されたテーブルが生成されます。この場合、IDは自動でプライマリーキーには設定されません。

## プライマリーキーにパーテンションキーとソートキーを設定
```:schema.graphql
type Customer @model @key(fields: ["email", "createdAt"]) {
    id: ID!
    email: String!
    username: String
    createdAt: AWSDateTime
}
```
fieldsのリストに2つ目を指定することで、ソートキーを設定することができます。

## 複合ソートキーの設定
```:schema.graphql
type Customer @model @key(fields: ["email", "createdAt", "username"]) {
    id: ID!
    email: String!
    username: String
    createdAt: AWSDateTime
}
```
さらに3つ4つとfieldsのリストを追加することで、複合ソートキーとして、設定できます。。
パーテンションキーは共通でいろんなソートを掛けたい場合は、この複合ソートキーの設定が便利です。

複合ソートキーを作成した場合は、複合ソートキー専用のカラムを自動生成してくれます。上記のテーブルの場合だと`createdAt#username`というカラムが自動生成されます。amplifyを使ったAppSyncを使う分には、特に意識することがありませんが、Lambdaで直接DynamoDBからデータの書き込みや読込をする際は注意が必要です。

また、複合ソートキーを作成するとcreatedAtだけでソートすることができなくなるため、こちらもまた注意が必要です。

## セカンダリインデックスを設定

```:schema.graphql
type Customer
  @model
  @key(fields: ["email"])
  @key(name: "username-index", fields: ["username"])
{
    email: String!
    username: String
}
```
たったこれだけで、usernameをプライマリーキーとしたセカンダリインデックスがusername-indexという名前でDynamoDBに自動生成されます。

## セカンダリインデックスを使った検索

```:schema.graphql
type Customer
  @model
  @key(fields: ["email"])
  @key(name: "username-index", fields: ["username"], queryField: "customerByUsername")
{
    email: String!
    username: String
}
```
これ本当に罠なんです。セカンダリインデックスを設定したら、それだけで検索できそうな感じするんですけど(Lambda関数からの検索はできます)、queryFiledをきちんと設定しないとセカンダリインデックスでAppSync経由のデータ取得できないんです。
例えば、`queryField: "customerByUsername"`を設定することで、以下のようなクエリが生成されます。

```:graghql/queries.js
export const customerByUsername = `query CustomerByUsername(
  $username: String
  $filter: ModelCustomerFilterInput
  $limit: Int
  $nextToken: String
) {
  customerByUsername(
    username: $username
    filter: $filter
    limit: $limit
    nextToken: $nextToken
  ) {
    items {
      email
      username
    }
  }
}
     
```

customerByUsernameを呼んであげることで初めてusernameでCustomerテーブルを検索して取得することができます。

# ポイント共有
理解するのに時間がかかったけれど大事なところ
## インデックスキーはnon-null
インデックスキーはnon-null設定が必要なので、設定する場合は `!`をつけ忘れないように。

## `@key`のあとから変更の罠
基本的にスキーマの変更はわりと簡単に行うことができますが、この`@key`さんだけは要注意です。細かく検証していない(ログ残しておけばよかった、、、)のでどういった場合にエラーが起きるか明確では有りませんが、わりとあとから変更するとエラーが起きてしまい、すべてのスキーマの更新が不能になります。
開発中であれば、ビルドアンドスクラップで何も問題有りませんが、リリース後にそんな事が起きたらと思うと、、、
これは結構、issueでも議論されていますが、未だに解決策は見つかっていないようなので、`@key`だけは本当に注意が必要です。
一応エラーが起きた場合は、実際DynamoDB側の設計は更新は一切されていないので、そっとスキーマを戻せば何もなかったことにはなります。

## セカンダリインデックスの複合ソートキーの罠
セカンダリインデックスでも複合ソートキーが設定可能なのですが、`name:  "category-Index"`というようにnameに `-`が入っているとエラーになるので注意が必要です。(DynamoDBのGUIでセカンダリインデックスを作成すると自動でnameに `-index`が付くので、その癖で命名すると痛い目に合います。)

```:schema.graphql
type Customer
  @model
  @key(fields: ["email"])
  @key(name: "categoryIndex", fields: ["category",  "age",  "createdAt"], queryField: "customerByCategory")
{
    category: String!
    email: String!
    username: String
    age: Int
    createdAt: AWSDateTime
    updatedAt: AWSDateTime
}
```

# エラー検証メモ
発見したエラーについてはここに随時追記していきます。

## テーブルのプライマリーキーを変更
パーテンションキーのみだったプライマリーキーにソートキーを加えて、`amplify push`を行うとエラーになります。テーブルのプライマリーキーを変更したい場合は、一度、テーブルごと削除した状態で `amplify push`を行い、その後、新たにテーブルを追加してから `amplify push`を行うことで更新することができます。なんなんだこのひと手間、、、。

## セカンダリインデックスのプライマリーキーを変更(2021/12/5更新)
~~テーブル同様にこちらもエラーになります。プライマリーキーを変更したい場合は、一度、その変更したい`@key`を削除した状態で `amplify push`を行い、その後、新たに追加してから `$ amplify push`を行うことで更新することができます。なんなんだこのひと手間part2、、、。~~

現在のAmplifyCLIのバージョンではもう削除と更新が複数になっても問題なくなっています。ただ、古いAmplifyCLIで作成したアプリのバージョンを上げていった場合は、起きます。

## 上記の通りにやったけど変更できない(2021/12/5更新)
~~一度の更新(ampliy push)につき、`@key`の複数の作成や削除はできない、というルールがあるようです。(2019/11/21時点)~~

```
Cannot perform more than one GSI creation or deletion in a single update
```

~~もう少し融通利かせてくれよ、と思いますが、まぁこの辺りは受け入れるしかないですね。なので、複数の`@key`を更新したい場合は、コツコツ1つずつ削除した後に、まとめて新たな`@key`を追加するか、いっそのこと完全にamplify remobe apiをして一から作り直すかのどちらかかなと思います。~~
~~`@model`のテーブルごと一度削除して、`$ amplify push`後、再度テーブル作成時に複数の`@key`をつけることは可能でしたので、コツコツorテーブル単位での対応のどちらかがベターです。~~

~~基本的な考え方として、`@key`はできる限り最初の段階で決めてから実装し、あとからは微調整を行う感じがいいと思います。~~

上記の項目の通り現在改修されています。


## 上記の通りやったけどできないPart2
```
Schema Creation Status is FAILED with details:
Failed to parse schema document - ensure it's a valid SDL-formatted document.
```
これは壮大にハマりました。エラーとしてはっきりとここが悪いよって言ってもらえれば対処できるのですが、上記のようなエラー分だと、どこが悪いのか検討がつかないので、消したり足したりを繰り返すしか無い、、、。
今回のエラーの要因は、インデックスキーとセカンダリインデックスキーで共通のパーテンションキーを使っていたためエラーが起きていました。セカンダリインデックスを削除し、インデックスキーのソートキーを複合ソートキーにして統一することで、解決しました。

え、これせっていできないとこまるんだけれど、、、

## ローカルセカンダリインデックス(LSI)の設定(2021/12/5更新)
~~上記でできないと言いましたが、変更時にはできないだけでできる方法がありました。~~

~~`@key`を使ったLSIの設定は、テーブルを初めて作成する時であればできます。LSIを設定したい場合は、初めorテーブルを削除後、１からの作成が必要です。
(言い方を変えると、インデックスキーで指定したパーテンションキーをセカンダリインデックスキーでパーテンションキーに指定することは、途中からはできません。)~~

こちらも現在のAmplifyCLIのバージョンでは途中から追加できるようになっています。そして、こちらも同様に古いAmplifyCLIで作成したアプリのバージョンを上げていった場合は、起きます、、、。

## インデックスのプライマリーキーにcreatedAtを指定した場合のエラー
createdAtはamplilfyさんが自動で書込をしてくれますが、インデックスのプライマリーキー(パーテンションキーorソートキー)に指定するとちょっとした対処が必要になります。
インデックスのプライマリーキーに指定→non-nullの設定が必須なので、 `createdAt: AWSDateTime!`と記載→ユーザーがcreate処理をする時に、createdAtを書き込めと怒られる→ `createdAt: new Date()`を渡すことで解決。
基本的には、createdAtとupdateAtはamplifyさんの自動書込なので、取得したい場合は、スキーマにcreatedAtとupdateAtを宣言すれば、特に書込は意識する必要はありませんが、インデックスのプライマリーキーに使う場合は、user自身での書込を忘れないようにしましょう。



▼LSIがよくわからない方はこちらをお読みください。
[DynamoDBの設計力をあげたい](https://www.ketancho.net/entry/2018/01/30/075500#%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E7%A8%AE%E9%A1%9E)


# おわりに
`@key`の登場により、amplifyさんのますますできることが増えてきました。今後のさらなる新機能追加に期待です。

# 参考
[公式ドキュメント](https://aws-amplify.github.io/docs/cli-toolchain/graphql?sdk=js)

# 関連記事
[AWS amplify フレームワークの使い方Part1〜Auth設定編〜](https://qiita.com/too/items/fc961283dcbef3aafdeb)
[AWS Amplify フレームワークの使い方Part2〜Auth実践編〜](https://qiita.com/too/items/54992bb871fc1a2ab101)
[AWS Amplify フレームワークの使い方Part3〜API設定編〜](https://qiita.com/too/items/c0ce14d16772b9f71223)
[AWS Amplify フレームワークの使い方Part4〜API実践編〜](https://qiita.com/too/items/f9078803ea394c35df7d)
[AWS Amplify フレームワークの使い方Part5〜GraphQL Transform @model編〜](https://qiita.com/too/items/ab6b313da9f9e34be4a7)
[AWS Amplify フレームワークの使い方Part6〜GraphQL Transform @auth編〜]
(https://qiita.com/too/items/fae2879ea36f00c3ae10)
[AWS Amplify フレームワークの使い方Part8〜GraphQL Transform @connection編〜](https://qiita.com/too/items/c77f1e5487d2497b5061)
[AWS Amplify フレームワークの使い方Part9〜Function 基礎編〜](https://qiita.com/too/items/d2732b3ec625011f4567)
[AWS Amplify フレームワークの使い方Part10〜Storage編〜](https://qiita.com/too/items/361bc10b6d27f133e9f8)
[AWS Amplify フレームワークの使い方Part11〜Function 権限管理編〜](https://qiita.com/too/items/b34cbfd7e96a01e33f23)
[AWS Amplify フレームワークの使い方Part12〜ENV編〜](https://qiita.com/too/items/5c1e28cf95fc6229547e)
[AWS Amplify フレームワークの使い方Part13〜Auth 設定更新編〜]
(https://qiita.com/too/items/52f35860bcb5bdf5e667)
[AWS Amplify フレームワークの使い方Part14〜Lambda レイヤー編〜]
(https://qiita.com/too/items/54de781085bd9a3a66d0)