知っている方からすれば「どうしてそんなこと?」と思うようなことも、知らない時にはつまづいてしまうもの。現在地とゴールが違えば、また通る道が異なれば、ぶつかる壁も違うでしょう。
私の前に立ちはだかった壁のお話がどなたかのお役に立てば幸いです。何枚もの壁の先にゴールはありました。
「心臓を捧げよ」
やろうとしたこと
まずは私の現在地とゴールです。
他の開発環境でREST APIを開発したことはあります。NoSQLもMongoDBを少々。今回はAmplifyを使って、REST APIを作ってみようと思います。
Amplify + RESTとすると、必然的に「Amplify + API Gateway + Lambda + DynamoDB」の組み合わせになることが分かりました。Lambdaは少し使ったことがありましたが、他は初めてです。
どんなものを作ろうか?
「トランプのカード52枚から何枚かのカードを選ぶ」というフロントがあり、そこで選んだカードの状態を「選ばれてない→選ばれた」に変えるAPIを作ってみようと思います。
壁1枚目:Dynamo DBの設計
NoSQL Workbenchの便利さと限界と
Dynamo DBの設計では、パーティションキーとソートキーを決めるのが肝要。どういう形でデータを引き出したいかから考えるべき。といったあたりを知識として仕入れる道すがら、「ローカルセカンダリインデックスは構築時でないと設定できない」といった、「NoSQLは項目を後から追加できる」という後付け指向 とは違う指向に戸惑いつつも、今回はいつでもamplify delete
してよい気楽さから、まずは始めることにしました。
それにしても、検討するのによい道具はないものかと調べていたら**「NoSQL Workbench」**というツールがあることが分かり、インストールしました。
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html
(小壁:私がダウンロードしようとしたときはなぜか日本語ページだとAccess Deniedになっていたのですが、その後は問題ないみたいです。)
Data Modelerとかあって、「検索してみるとこんな感じ」とか分かって、「Dynamo DB定義は初回一発勝負」の気持ちが少し楽になりました。
パーティションキー(およびソートキー)で、特定できる必要があるらしいので、カードの名前(ハートの1とか)をパーティションキーとすることに。そうなると選ばれているか否かをソートキーにすればいいか?とそのような定義に。
amplify add api
で、APIとLambdaとDynamoDBの定義ができあがっていきます。このときは、CUIの質問への回答に必死でそう思わなかったのですが、これをAWSコンソールから設定することを考えたら確かに楽~というのは後から感じました。
(小壁:amplify add api
で選択肢とか間違えると戻れないので、事前にメモ作って見ながら設定しました。NoSQL Workbenchから「設定を投入!」とかできないの?とか思いましたが、徐々に諦めました。)
壁2枚目:Ampifyが生成したREST APIの実行
APIがなんかできたっぽい!?NoSQL WorkbenchのOperation builderで、作成したDynamo DBに接続して。。。そりゃまだデータ空ですわ。52件のデータ投入をどうしようかと考えたのですが、aws dynamodb put-item
使ってシェルで投入することに。投入後のデータはNoSQL Workbenchで表示。。。できた!
そもそもの呼び出し方は?
RESTなんですからパスを指定すればいいのは分かってますし、amplify add
で確かにパスも指定しました。
GET /パス
でGETできるのかしら?API Gatewayのコンソールには定義したパスも表示されていますし、メソッドテストから実行してみましょう!
結果は404エラー。。。探し回った挙句、以下のページを見つけました。
https://docs.amplify.aws/cli/restapi/restapi/
GET /items/[ID]
で、IDにパーティションキーを設定しろと。
できたけど、状態に絞って全件欲しいとかだったらどうすればいいの?(壁3枚目に続く)
PUTできない。。。
PUT /items
で値を設定できると。えーっと値はどう設定すればいいのでしょうか。。。?
この頃になると、backend/function/hoge/app.jsがLambdaのソースであることが分かっていたので、直接見てみると。。。
req.bodyで指定するらいしい。
メソッドテストでbody指定できるじゃん、これで。。。できない。パーティションキーが指定されていないとエラーが出てる。
API Gatewayをご存じの方であれば「そりゃそうじゃん」という理由なのかもしれませんが、ヘッダーに
Content-type: application/json
と指定しないとJSONデータとして解釈してくれないそうで。
壁3枚目:GSIを使った検索はどうすればいい?
「状態に絞って全件欲しいとかだったらどうするの」って問いは、最初の「どういう形でデータを引き出したいか」の考慮不足が露呈しただけ、ではありますが。。。
先のドキュメント
https://docs.amplify.aws/cli/restapi/restapi/
によると、どうあってもパーティションキーは指定しろ、とのことなので思い出したのがLSIとGSI。
セカンダリインデックスのパーティションキーとして「カードの選択状態」にしたら実現するのではないかと。恐る恐る
amplify update storage
したら、GSIは追加できるのでここで設定。
(避けてしまった小壁:そういえばLSIを設定しますか?という質問はamplify add api
で出てこなかったような。。。)
Ampllifyが生成したLambda関数の書き換え方
GSIを設定はしましたが、でどうやって検索すればいいのさ!と。そして見つけたのがこちらの記事。
https://gerard-sans.medium.com/create-a-rest-api-integrated-with-amazon-dynamodb-using-aws-amplify-and-vue-5be746e43c22
部分的であれば他にも情報はあると思いますが、API GatwayからLambda関数の書き方を1つの流れで書いているのは少ないかもしれないです。
メソッド名、パスで関数が特定され、パラメータを構築してAWS.DynamoDB.DocumentClientのメソッドに渡す。
この辺りからようやく楽しくなってきた感じです。app.js書き換えると変更を検知してamplify push
で反映される。素晴らしい!
GSIを使った検索は(REST的には反則かもしれませんが)、GET /パス
にリクエストパラメータで状態を指定するとGSIのパーティションキーで検索するようにしました。
(小壁:Limitとか指定できるはずなのにどうするの?と思っていたのですが、同じようにリクエストパラメータで指定することにして、メソッドに渡して実現できました。)
デバッグってどうするの?
これもLambda使っている方ならば常識なのかもしれませんが。。。
CloudWatchのログイベントにconsole.logで出力したものが出力されるんですね。Lambdaのコンソールから辿れます。
壁4枚目:updateできない。。。
指定したカードの状態をまとめて変更したい。メソッドとして適切なのはupdateでしょうか。
まとめて指定するバッチ的なメソッドもありますが、まずは1つずつループで更新する方法をとってみます。
あれ?更新されませんね。。。
amplifyが生成したPUTを使ってみると更新されます。
(小壁:putは一致したデータを更新する、というより入れ替えます。指定しない項目があった場合は削除してしまいます。)
何が違うんだろう。。。
まとめて処理する=関数として括りだし、と条件反射で作ったんですが、、、というところで気が付きました。
「Promise」
amplifyで生成されたapp.jsではコールバック関数を定義しているのですが、下記を見るとawaitだのpromise()とか書いてる。。。
https://docs.amplify.aws/guides/functions/dynamodb-from-js-lambda/q/platform/js/#creating-an-item-in-dynamodb-from-lambda
この期に及んで、DocumentClientのドキュメントを読むと、戻り値のメソッドにpromise()があって、'thenable' promiseを返すとのこと。
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html#promise-property
配列にpushして、Promise.allに渡して。。。できた!
(実は直前まで、Promise.allをreturnしていなくて、呼び出し元が先行して終了していたのは内緒です。。。)
(昔からの壁:フロントはJavaScript一択なので、バックエンドもJavaScriptを選んでみるのですが、非同期処理に引っかかるたびにpythonの方がいいのかなと考えてしまいます。)
ここはゴールなのか?
3枚よりも多かった壁を乗り越え、ようやく考え通りのAPIが1つできました。
でも本当のゴールは壁の中ではなく外にあり、壁を乗り越えることは手段であって目的ではないのです。
ゴールを目指す旅は続きます。