AWS Japanで1年経った
4月1日でAWS Japanに入って1年が経ちました。とても早かったです。
ネットワークエンジニアとしてキャリアが始まり、自宅サーバに固定IPを8本引いてXenとKVMで仮想インスタンスをホストして知人に有料で貸してたら海の向こうからやってきたEC2の登場に腰を抜かし、その後バックエンドエンジニアになり、Androidエンジニアになり、10年の時を経てAWSを使ったお仕事をしているのは感慨深いものがあります。
AWS Japanで働いていてよかったことは沢山ありますが、これまでとは比較にならない深度でAWSのことを理解し、様々な生のユースケースに触れられるところです。AWSのスキルは言ってしまえばいちクラウドベンダの知識に過ぎませんが、それが世界中で使われているので事実上世界で通用するスキルを身に着けていることになるのは素晴らしいことです。
僕のキャリアはスタートアップといわゆるウェブ系企業を渡り歩いてきたので、比較的シンプルな構成のアプリケーションに関わることが多かったですが、AWS Japanの技術者として色々なお客様と仕事をしていると、かつての自分には想像もつかなかったような難しい要件を求められることが少なくありません。その時に、どんな難しい場所にもぴったり合うブロックのピースを見つけられるのは、今なおAWSをおいて他にないというべき境地にあると感じます。
21世紀も20年代に突入し、エンジニアとクラウドの関わり方もまた少しずつ変わりつつあります。今年はぜひとも、自分の最大の強みであるモバイル開発の観点からもAWSの良さを伝えていけたらなと考えています。今年も宜しくお願いいたします!
そうそう、僕のチームは次の2ポジションを熱烈募集しています。ご興味のお有りの方は是非お声がけください!それでは。
心のローパスフィルタ
地殻を毛細血管のごとく駆け巡る光ファイバが空間の垣根を取り去った結果、四半世紀前なら触れることすらなかった情報に晒されるようになった。 これは幸福でも不幸でもある。
観測する限り、多くの人は自分の置かれている環境の平均値で構成されている。朱に交われば赤くなるというやつだ。 これは環境のお陰で自分が周りのレベルまで引き上げられる例も、自分自身がいわば浸透圧の合った塩分濃度に自然と落ち着く例もあるだろう。 とにかく、環境から著しく外れた存在に触れるには周囲の人間を何ホップもしなければならなかった。
ところがインターネットが魔界の界峡トンネルのように突然1ホップで次元の違う人々に出会うことを可能にしてしまった。その結果、ブラウザ越しの玄関先にはS級妖怪がゴロゴロ歩いている。テクノロジーの進化はいつも喜ばしい。問題はこれにどう向き合うかだ。
ある種の人間にとって―まあこれは僕のことなのだが―半径30m以内にいる*ように見える*人たちは、自分が到達すべき平均目標に見えてしまうことがある。こうなると不幸だ。本来は界峡トンネルを越えないと出会うことすらできなかった文字通り次元の違う存在が当然にできることを、僕はどうして努力してもなお達成できないのだろうと思い悩むことになる。立っている土台も、越えてきた屍の数も、これから対峙する課題のスケール感も違うにも関わらずだ。
結局の所、昔からお母さんに言われる「他所はよそ、家はうち」に行き着く。どこかで心のローパスフィルタで余りにも高い理想をカットしないと自分の足元もおぼつかない。インターネットは簡単に他人の人生を摂取することができ、それはアルコールのようなエンターテインメント性と常習性をもたらすが、あくまで他人の物語だ。慎ましくも愛らしい自分の人生を、諦めるのではなく、前向きに受け入れなければならない。自分の他に自分の人生の面倒を見てくれる人はいないのだから。そんなことを考えていた。
肉体と精神の不可分性
一週間ほど出張で非常に寒い地域にいて、不運にもインフルエンザに罹患してしまい寝込んでいた。ようやく帰国できたが、身体的にも精神的にも参ってしまった。
元々体力には自信があって、この10年ぐらいインフルはおろか風邪で休むことすら稀だったし、昨年9月で酒もやめたので「二日酔いで翌日を棒に振る」みたいな現象もまったくなくなって、健康とは完全にコントローラブルなものになっていたはずだった。
ところがいざ39℃の熱で数日間夢うつつの生活を送ると、健康というのがどれほど得難いものであったかという当たり前の事実を思い知らされた。とりわけ、肉体的健康とは完全に独立して存在していると思っていた精神的健康というものが、肉体的な衰弱によりあっさりと共倒れになったのは大きなショックだった。知力は体力に依存しているのだ。考えもしなかった。
久しぶりに出社して懸垂機にぶら下がってみると、果たして体が持ち上がらない。あれだけ毎日取り組んでいたことが、砂上の楼閣ように一夜にして失われてしまった。また、毎朝3時に起きてコツコツつづけていたアルゴリズムクイズと数学も、いまは何をやっていたのか思い出せないほどだ。特に極度に頭を使う作業が先々週ぐらいから絶不調で、一体どうなってしまったのか自分でも呆然としていたのだが、思えば病気の兆候だったのだろう。体力的にはようやく回復したが、まだ頭の中は蜘蛛の巣がかかったように茫漠としている。いまも言葉がうまく出てこない。
これまで、自分に対する投資はもっともROIが高いという信念をもって絶えず勉強し続けてきた。結果的に、努力はすべてを叶えないが、自分がなりたいソフトウェア技術者ぐらいはかならず努力でたどり着けるという確信を持つに至った。ところが、このような前提は肉体的な健康というものに大きく依存していることに図らずも気付かされた。これから30代後半を過ごし、40代に突入するにあたって、この基盤はますます脅かされる可能性が高い。これまでのように、ただ盲目的に自分のやれることをやってさえいればどこかにたどり着けるという確信がすこし揺らいでしまった。自分がいまの状況にあるのは、単に幸運だったのだ。慎まねばならない。
また近い将来、怨念にも似た執念の火が自分の中で燃え始め、再び元の自分に戻れる日がくると信じているが、それまではほんの少しだけ無茶な早起きをやめてしばらく自分の健康と向き合いたいと思う。命あっての物種。人生は長く短い。このバランスが最も難しい。それでは、近いうちにまた。
レッテル貼りを嫌っていた自分がレッテル貼りモンスターになっていた話
ここのところずっと考えていたことが自分の中でまとまってきたので言語化する。
僕の非常によくない傾向として、自虐的・自罰的な物言いをするという点がある。たとえば、
- CS学位がないので何をやってもダメ
- 算数がわからないので競プロやる資格なし
- 英語サッパリできない、TOEIC900点は現地校の幼稚園なみ
というようなやつだ。これは正直に言って、自分の中では経験に基づく事実のようなもので、いずれも「だから何とかしなくては!」という部分が自分に対してかかっているのだが、言い方が稚拙すぎて、同一属性の全人類に対して銃口を向けていることに気付いていなかった。
「僕は文系なので…」みたいな自虐もそうだ。そもそも自分がこういうレッテル貼りに対して人一倍憤慨していたはずなのに。
みんなっ!高校数学を諦めた人という意味で文系という言葉を使うのをやめてくれー!傷つくんや!こんなん俺でも2分で暗算できるぞ… pic.twitter.com/kZIIHXxxN5
— 父🌖 (@fushiroyama) 2019年9月14日
この画像が一時期Twitterで流行っていて「文系はこんなのも解けないのか」みたいなdisりをみるたびに、微分の導関数ぐらい導けるわいふざけんなよという思いがあった。"文系"を無学のエイリアスとして使うんじゃないよと。これは、ともすれば"文系"を自分に対する言い訳として使っていた軟弱な態度と完全に矛盾する。それに、理転して情報科学の大学院に進学した今では、この言葉は自虐としてすら機能していない。自分が最も嫌い、軽蔑していたレッテル貼りを自ら進んで行うモンスターになっていたということだ。いまは自分を恥じている。
US駐在したときもそうだった。日本では謎のコンサルが「シリコンバレーのエンジニアは年収2000万!」みたいな物価も何も考えてねえだろみたいなことを言うのを見て疲弊し、たまに現地就職の日本人にまで「駐在組はクビにもならないし家賃補助もあっていいですよね」と謎の憎しみを向けられたりしたものだ。どれも実態を伴わないレッテル貼りによるものだ。全部自分が嫌っていたやつだ。現代のイソップ童話の登場人物に自分がなってしまっていた。
僕個人としては次の点に気を付けたい。
- 自分の属するカテゴリについて言及しない。常に主語は自分とする。
- 自虐を控える。
- 何かを貶めない。批判は建設的対案とともにする。
自分の娘に見られて恥ずかしくないことだけインターネットに書こうと思います。それではポケモン捕まえてきます。
とにかく次の10年を生き残りたい
ついに2020年になってしまった。
この10年はとにかくネイティブアプリ開発にすべてを投資した。
京都GTUGのイベント情報によれば、第1回Androidハッカソンは2009年9月5日に開催されている。このハッカソンのためにドコモ HT-03Aを契約しに行ったのがAndroidとの出会いだった。当時APIレベル3、コードネームCupcake、バージョン1.5である。加速度がホントにJavaのコードから取れてびっくりしたのを覚えている。
実に丸10年の月日が過ぎた。スマートフォンの普及率は20-30代で90%を超え、今後も低くなるということはないだろう。当然、それらスマートフォンで動作するネイティブアプリ開発の需要もしばらくは続くに相違ない。ただし、開発の前線から見える情景は少しずつ変わってきているようにも感じられる。
3-4年前だとネイティブファースト(特にiOS)が当然であり、ウェブが存在しないサービスも少なくなかったが、ウェブへの回帰も見られるようになってきた。また、FlutterによるiOS/Androidアプリ同時開発も流行しているように思う。この辺りは個人の感覚だし、東京とそれ以外、あるいは米国、欧州でもかなり違っているのだろうと想像するが、本ポエムの主眼ではない。
次の10年は何に投資すればよいのだろうか。本当に知りたい。僕は深刻にこの辺りの嗅覚が鈍い。
iPhoneがリリースされた2007年当時、僕はすでにプログラマだった。当時所属していた会社はSkypeのようなP2P通信アプリを開発しており、LinuxザウルスやSymbianOSなんかにも対応していた記憶がある。その渦中にあって、iPhoneがその後天下を取ることを想像できなかった。BlackBerryやNokia端末との違いが分からなかった。当事者とは案外そんなものだ。何回目かのxR元年と、いつ来るかわからないシンギュラリティ。また同じように、10年後振り返って「ああ、なんであのときアレに投資していなかったのか…」と嘆息するのだろう。
次の10年は僕の40代を捧げる10年となる。実際に35歳を過ぎてみて、新しいことに興味を持って勉強をし続ければまだまだ全然戦えるという感覚はある。理論や研究を補うために大学院にも進学した。競プロも始めた。酒もやめた。ただまあ、抗いきれない事実として体力は落ち続けているし頭の回転も少しずつ鈍っている。圧倒的な瞬発力の戦いから、総合格闘技で最後は寝技で絞めるみたいな感じになってきている。次の投資先は慎重に決めねばならない。
一旦、2020年Q1は再びウェブフロントエンドへの投資を少し増やしてみようと思っている。2013年まではフロントエンド開発(なんと、jQueryとbackbone.js)もしていたのだが、7年も経ってしまって自分の知識はバクテリアに分解されて石油となっている。その間ウェブ技術は目覚ましい発展を遂げ、現在のJSは当時とは完全に別物だと感じる。TypeScriptで型の恩恵も受けられる。ありがたいことだ。この状況を作ってくれた人々に感謝したい。
という訳で何卒諸々ご指導ください。本年も宜しくお願いいたします。
アルゴリズムと数学的思考力
厳しい。年始早々厳しさを感じている。自分のプログラミング力にだ。伸び悩んでいる。
端的に言って、数学力のなさが自分のプログラミング能力に制限をかけている。例えばこの問題。
560. Subarray Sum Equals K
入力として与えられる配列 nums
のうち、合計が k
となる部分配列の個数を数え上げよ。どうも有名な問題らしいが…
まず大前提として、部分配列なので i, j
の2重ループで始点・終点を定めて sum(nums[i, j]) = k
になるものを数え上げれば必ず答えが得られる。最悪計算量は O(N^3)
ただし i < nums.length < 20000
という制約があるので N^3
では遅すぎるから何か考えてくださいというのがスタート地点。
ここで、結果の変わらない累積和を何度も求めているので nums[i, j] = k
を求めたい場合、 nums[0, j] - nums[0, i - 1]
としてこれまでの計算結果を利用できることに気付けると計算量を大幅に減らせる。ただし、nums
には負数が含まれているので同じ累積和が何度も出現する可能性があるので Map
で出現頻度をカウントする
public int subarraySum(int[] nums, int k) { int j = 0; Map<Integer, Integer> seen = new HashMap<>(); seen.put(0, 1); int counter = 0; for (int num : nums) { j += num; int i = j - k; if (seen.containsKey(i)) { counter += seen.get(i); } seen.put(j, seen.getOrDefault(j, 0) + 1); } return counter; }
これで時間計算量 O(N)
空間計算量 O(N)
で通った。
seen.put(0, 1)
の部分だけトリッキーで、このアルゴリズムがそもそも前の計算結果を利用するものなので、初めて sum(nums[0, j]) == k
になるケースをカバーする。
これってもはや算数の問題で、区間 nums[i, j] = k
が nums[0, j] - nums[0, i - 1]
で求まるっていうのは答え見て初めてマツコデラックスが感心する時の声で「ハ〜〜〜〜〜〜!」って唸ることしかできなかった。もう1題。
373. Find K Pairs with Smallest Sums
ソート済み配列 nums1
, nums2
と整数 k
が与えられる。このとき nums1
, nums2
から1つずつ取り出してつくるペア (u,v)
を、和が小さい順に k
個取り出せ。
Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3 Output: [[1,2],[1,4],[1,6]]
これも O(N^2)
で全ペアが列挙でき、それを整列するコストが O(N log N)
なので最悪計算量 O(N^2)
で必ず解ける問題。これには特に nums.length
, k
の制約が示されていないが、AtCoderなどのコンテストでは間違いなく O(N^2)
では制限時間に収まらない(=不正解となる)データが与えられるだろう。したがってもっと高速なアルゴリズムを考えなさいというのが趣旨となる。
ここで nums1
, nums2
はソート済みなので、nums1[0]
, nums2[0]
は常に最小のペアであることが保証される。このような最小のインデックスをそれぞれ i, j
とした場合、nums1[i], nums2[j]
が解答のひとつならば nums1[i + 1], nums2[j]
か nums1[i], nums2[j + 1]
が次の答えの候補になる。言われてみればそうである(孫権顔)
後は候補が複数あった場合に低コストで最小値を得られるデータ構造を考える。そう、二分ヒープ(最小)である。min heap
の追加・削除の最悪コストは O(log N)
なので、今回要素数 k
の候補から常に最小を取り出しつつ k
個集めるまで繰り返すので O(K log K)
が模範解答のひとつとなる。本件は競プロ界で著名な 宇宙ツイッタラーX さんに解説いただいて初めて理解できた。スレッドを参照されたい。
num1をa、num2をbとします。
— 宇宙ツイッタラーX (@kenkoooo) 2019年12月31日
aとbはソートされているので、a[i]+b[j]<=a[i]+b[j+1] と a[i]+b[j]<=a[i+1]+b[j]です。
なのでa[i]+b[j+1]かa[i+1]+b[j]が答えに含まれる時、a[i]+b[j]は必ず答えに含まれます。a[i]+b[j]が答えに含まれない時a[i]+b[j+1]とa[i+1]+b[j]は答えに含まれることはありません。
実装例も、氏のそれより綺麗には到底書けないので 氏のgist を見ていただきたい。
で、ここからが主題なのだが、これらの問題はプログラミング能力というか算数の問題なのだ。最初の問題はデータ構造と言っても HashMap
ぐらいしか使ってないし、2題目も min heap
こそ確かにコンピュータ・サイエンス的なデータ構造ではあるものの、これは最小(最大)値を O(log N)
で出し入れできる(最小(最大)値の参照だけなら O(1)
)データ構造とだけ覚えていれば使える類のものだし、そこがこの問題を解く最大のキーというわけではない。現に僕は min heap
を使うと模範解答になるというヒントを貰った後も「 nums1[i], nums2[j]
が答えならば nums1[i + 1], nums2[j]
か nums1[i], nums2[j + 1]
がその候補になる」という核心部分が出てこなかったし、むしろここがこの問題の最大のキーポイントであるわけで。
ここからは自分でもうまく整理できていないのだが、これは「数学力」なんだろうか。僕が数学が苦手なのは自他ともに認めるところだが、いま大学院の授業についていくためにマセマの微分積分とかを解いてるけど、なんというかこう、公式あてはめて微分するとかとは違うわけじゃないですか。使われているのは中学生レベルの四則演算で、むしろ「数学的思考力」を問われているというか。
社会人になってプログラマとして10年ぐらいやっていて忘れかけていたけど、そういえば僕は勉強まったくできなかったな、受験敗者だったわ、忘れてた。僕には数学的思考力が致命的にない。こういうのをアルゴリズムクイズを解いていると嫌というほど思い出させられる。
今回の2題は非常に悔しい思いをしたので覚えたが、これを繰り返していけば僕のように頭がよくない人間も類題を解けるようになるのか、それともちょっとひねられるだけでまたまったくお手上げになってしまって悔し涙に枕を濡らすのか、いまは分からない。願わくは2年後に読み返したときに「あの時はあんなしょうもない問題で悩んでいたんだなぁ😅」と思えるようになっていたいが、どうやることやら。正月に書くような内容ではないな…
Amplify for Androidでより直感的になったGraphQLを試した
TL;DR
本エントリはAWS Amplify Advent Calendar 2019の24日目です!
今回は新しくリリースされたAmplify for Androidを使って、前回と同じく下のようなチャットアプリを作るので、興味のある方は前口上を飛ばして後半をお読みください🎄
Amplifyとは
本題に入る前に、Amplifyを最近よく耳にするようになったけど何かよく分かっていないという人向けの説明をします。
Amplifyはモバイルバックエンドを爆速で作るためのサービスです。 https://aws.amazon.com/jp/amplify/
他のいわゆるモバイルバックエンドとの際立った違いのうち、個人的に強調したいのは以下2点です。
- バックエンドはAWSのサービスであり、真の意味でスケールする
- GraphQLのマネージドサービスを利用できる本日時点で唯一のプラットフォームである
Amplifyの良い点として、フロントエンドから利用する時はそれと意識せずに簡単に構築できるけれども、バックエンドはAWSが個別に提供するサービスで構成されているという点です。これは、バックエンドをキャパシティのよく分からないブラックボックスとして扱うことなく、必要であればAWSを使い慣れたインフラ部隊と協力して本当の意味でスケールするようにチューニング可能だということです。
さらに、AmplifyはGraphQLのマネージドサービスを提供する今日時点で唯一のプラットフォームです。GraphQLの良さは他の記事に譲りますが、これを自前で構築するのは中々骨が折れます。AmplifyでAPIを追加するときにGraphQLを選択すると、裏ではAWS AppSyncが使われますが、これはGraphQLのマネージドサービスであり、サーバのプロビジョニングやチューニングを気にすることなくGraphQLのメリットを享受できます。
Amplifyの構成要素
Amplifyは主に次の要素で構成されています
後述しますがAmplify for iOS/AndroidのリリースでXcode/Android Studioとの統合がすすみ、CLIを単独で使用することは今後かなり少なくなります。したがって、基本的には普段開発しているときのようにCocoaPodsやGradleでライブラリを導入するように使い始めることができます。具体的な使い方はこのエントリの後半で解説します。
Amplifyのカテゴリ
Amplifyはカテゴリという概念があり、
- API…REST APIやGraphQL
- Auth…認証と認可
- Storage…ストレージ
- Analytics…分析とユーザエンゲージメント
- Predictions…AI/MLの組み込み
- XR…AR/VR
のような機能のうち、使いたいものを好きなだけ選んで使うことができます。
これまでもモバイルからAWSのサービスを使うことは当然できました。これはAWS Mobile SDKによって簡単に実現できます。 ただ、これはどちらかと言えば「AWSのこのサービスを組み込みたい」というマインドセットで使います。対してAmplify for iOS/Androidでは「APIを組み込みたい」というように、ユースケースの側面から使うことができます。
前置きが長くなりました。ここまでがAmplifyの概要です。次からはAmplify for iOS/Androidについて解説します。
Amplify for iOS/Androidとは
先日のAWS re:Invent 2019で新しく発表された、Amplifyをモバイルからより簡単に使うためのアップデートです(公式ブログ)。
先日、上記エントリで AWSAppSyncClient
を使ってGraphQLにつなぐ方法を紹介しましたが、これが新しいAPIでどんな感じになるのか見てみましょう。
注意点
公式サイトに明示されていますが、Amplify for iOS/Androidはプレビューリリースで、本番環境での利用はまだ想定されていません。他の多くのOSSがそうであるように、開発者が想定していないと思われることをやろうとするとまだ粗削りな部分が見て取れますが、コントリビューションのチャンスでもあります!本エントリで興味を持ってくださった方は是非お試しください!
Androidクイックスタート
それではAmplify for Androidを使ってGraphQLの読み書きをしてみたいと思います。序盤は公式サイトの手順に準じます。
まずはプロジェクトの build.gradle
にプラグインをインストールします。
buildscript { ext.kotlin_version = '1.3.61' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Amplify classpath 'com.amplifyframework:amplify-tools-gradle-plugin:0.1.0' } } // Apply Plugin apply plugin: 'com.amplifyframework.amplifytools' allprojects { repositories { google() jcenter() } }
次にアプリケーションレベルの build.gradle
にJava8の設定と依存関係のインストールをします。
android { compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } } dependencies { implementation 'com.amplifyframework:core:0.9.0' implementation 'com.amplifyframework:aws-api:0.9.0' }
次に、Android StudioをProjectビューにして amplify/backend/api/amplifyDatasource/schema.graphql
を編集します。これはGraphQLのスキーマ定義で、これを元にAndroid用のモデルが自動生成されます。せっかくなので、以前のエントリと同じくリアルタイムチャットアプリを開発してみます。まずは、前回と同じ次の定義で作ってみましょう。
type Message @model { id: ID! username: String! content: String! }
次に、Amplify CLIをインストールします。npm
を使うのでNode.jsが入っていない人はインストールしてください。
$ npm install -g @aws-amplify/cli $ amplify -v 4.7.0
初めてAmplifyを利用する場合は、CLIにAWSのクレデンシャルを設定する必要があるので amplify configure
コマンドを実行します。AWSのマネジメントコンソールにリダイレクトされるので、そこでユーザを作成してキーを発行してください。ここは次のチュートリアルに非常に詳細な解説があります。
この状態でAndroid Studioに戻ってくると、Build > Make Project
でプロジェクトをビルドします。
すると、Graldeメニューに modelgen
と amplifyPush
のタスクが追加されます。
まず、modelgen
を実行すると先程のGraphQLのスキーマから com.amplifyframework.datastore.generated.model
にメッセージを表すモデルの Message.java
が生成されます。
次に amplifyPush
を実行すると、Amplify CLIが必要なバックエンドをプロビジョニングしてくれます。この作業は数分かかることがあります。
完了したら、これらを使ってチャットのメッセージを読み書きしてみましょう。
最初にAmplifyを初期化するための設定をします。カスタムApplicationクラスを作成し、次の内容を追記します。
class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Amplify.addPlugin(AWSApiPlugin()) Amplify.configure(applicationContext) } catch (e: AmplifyException) { Log.e(TAG, e.message) throw RuntimeException(e) } } }
MyApplication
は AndroidManifest.xml
に追加するのを忘れないでください。
<application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
あとは読み書きをするだけです!まずはミューテーションから。
modelgen
によって作られたモデルは Message.builder()
のようなビルダーがついてきます。ユーザの入力に合わせて簡単にモデルを組み立てることができます。
あとは Amplify.API.mutate
メソッドを呼ぶだけです。結果を受け取りたい場合は ResultListener
も一緒に渡します。
private fun mutation( username: String, content: String ) { val message: Message = Message.builder() .username(username) .content(content) .build() Amplify.API.mutate<Message>( message, MutationType.CREATE, object : ResultListener<GraphQLResponse<Message>> { override fun onResult(response: GraphQLResponse<Message>) { response.data?.let { // do things with message } } override fun onError(e: Throwable) { } }) }
次はクエリです。こちらも Amplify.API.query
メソッドを呼ぶだけです。
結果は同じように ResultListener
で受け取れるので、これを RecyclerView
で描画するといった使い方が一般的でしょう。
private fun query() { Amplify.API.query( Message::class.java, object : ResultListener<GraphQLResponse<Iterable<Message>>> { override fun onResult(response: GraphQLResponse<Iterable<Message>>) { val messages: MutableList<Message> = mutableListOf() for (message in response.data) { messages.add(message) } // update RecyclerView updateMessages(messages) } override fun onError(e: Throwable) { } }) }
最後にサブスクリプションです。なんとこちらも Amplify.API.subscribe
メソッドを呼ぶだけです。
これだけでメッセージの作成をリアルタイムに通知してもらえるので、チャットアプリのようなものがいとも簡単に作成できます。
private fun subscribe() { Amplify.API.subscribe( Message::class.java, SubscriptionType.ON_CREATE, object : StreamListener<GraphQLResponse<Message>> { override fun onNext(response: GraphQLResponse<Message>) { response?.data?.let { // update RecyclerView addMessage(it) } } override fun onComplete() { } override fun onError(e: Throwable) { } } ) }
めちゃくちゃ簡単ですね!ここまでが公式のチュートリアルに相当する内容です。
時系列でメッセージを取得
ここからは少し発展的な内容を扱います。
AmplifyでGraphQLを使う場合、デフォルトではAppSyncとDynamoDBが使われます。DynamoDBは扱うデータサイズや規模によらず、うまく設計すればミリ秒単位のパフォーマンスを維持できるKVSおよびドキュメントデータベースですが、一般的なRDBMSを1台だけで扱うときとは違う点を考慮する必要がある場合があります。たとえば「メッセージを投稿順で取得し、ページングさせたい」という場合には、DynamoDBではいくつか実現方法がありますが、
- 何か一意なパーティションキー
- 日付をソートキー
として設計するのがよくあるパターンです。ここではDynamoDBの詳細なデザインパターンには触れませんが、チャットサービスといえば普通はチャンネル(部屋)を複数持てるのが普通なので、各部屋を表現する roomId
+ メッセージの日付 createdAt
を使ってメッセージを時系列に取得することにします。
まずは前出のGraphQLのスキーマを次のように変更します。
type Message @model @key(name: "SortByCreatedAt", fields:["roomId", "createdAt"], queryField: "listMessagesSortedByCreatedAt" ) { id: ID! username: String! content: String! roomId: String! createdAt: AWSDateTime! updatedAt: AWSDateTime }
name: "SortByCreatedAt", fields:["roomId", "createdAt"]
の部分で時系列に取得するためのキーを追加しています。
また queryField: "listMessagesSortedByCreatedAt"
の部分で、このキーを使ったクエリをする際の名前を指定しています。
できたら、以前同様 modelgen
と amplifyPush
を実行してモデルファイルを再生成し、バックエンドにプロビジョニングします。
これで、GraphQL的には次のようなクエリで時系列で取得できるはずです。
query GetPost( $roomId: String! $limit: Int! ) { listMessagesSortedByCreatedAt( roomId: $roomId sortDirection: ASC limit: $limit ) { items { id content username roomId createdAt } } }
ただし、現状のAmplify for Androidではこのような任意のクエリを簡単に送る方法がないので、GraphQLRequest
を利用します。
GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())
のように使います。
注意点として、スキーマで createdAt: AWSDateTime!
のように定義した場合、自動生成された Message
クラスでは日付は java.util.Date
型として生成されますが、このモデルを透過的にGraphQLのクエリに変更してくれるライブラリ組み込みの GsonVariablesSerializer
は java.util.Date
を雑に yyyy-MM-dd
に変換します。
これでは精度的に問題なので、ISO 8601
にするために独自の GsonVariablesSerializer
を作成します。
class GsonVariablesSerializer : VariablesSerializer { override fun serialize(variables: Map<String, Any>): String { return GsonBuilder() .registerTypeAdapter( Date::class.java, DateSerializer() ) .create() .toJson(variables) } internal inner class DateSerializer : JsonSerializer<Date?> { override fun serialize( date: Date?, typeOfSrc: Type, context: JsonSerializationContext ): JsonElement { val df: DateFormat = SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault() ) return JsonPrimitive(df.format(Date())) } } }
あとは次のようにクエリを組み立てて、GraphQLRequest
を作成して Amplify.API.query
します。
残念ながらKotlinのraw stringsでは$
自体の文字として扱えないので${'$'}
のようにしています。
val query = """ query GetPost( ${'$'}roomId: String! ${'$'}limit: Int! ) { listMessagesSortedByCreatedAt( roomId: ${'$'}roomId sortDirection: ASC limit: ${'$'}limit ) { items { id content username roomId createdAt } } } """.trimIndent() val variables: Map<String, Any> = mutableMapOf( "roomId" to DEFAULT_ROOM, "limit" to DEFAULT_LIMIT ) val request = GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer()) val listener = object : ResultListener<GraphQLResponse<Iterable<Message>>> { override fun onResult(response: GraphQLResponse<Iterable<Message>>) { val messages: List<Message> = response?.data?.map { it }.orEmpty() // do things with messages } override fun onError(e: Throwable) { } } Amplify.API.query(request, listener)
ミューテーションもまったく同様です。
val query = """ mutation CreateMessage( ${'$'}id: ID! ${'$'}content: String! ${'$'}roomId: String! ${'$'}username: String! ${'$'}createdAt: AWSDateTime! ) { createMessage(input: { id: ${'$'}id content: ${'$'}content roomId: ${'$'}roomId username: ${'$'}username createdAt: ${'$'}createdAt }) { id content roomId username createdAt } } """.trimIndent() val variables: Map<String, Any> = mutableMapOf( "id" to id, "content" to content, "roomId" to roomId, "username" to username, "createdAt" to createdAt ) val request = GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer()) Amplify.API.mutate( request, object : ResultListener<GraphQLResponse<Message>> { override fun onResult(response: GraphQLResponse<Message>) { } override fun onError(e: Throwable) { } } )
これで、時系列にメッセージを読み書きするための対応ができました。
改善を望む内容
最後に今回気付いたいくつかの今後改善されるであろう内容を取り上げます。
まず、Amplify.API.query
, Amplify.API. mutate
, Amplify.API.subscribe
はいずれも現在のところブロッキングなAPIとなっています。これは、Loader
や Observable
等でラップして扱うことを意図しているのか、それとも単に設計上のミスなのか判然としません。とにかく、このままではUIをブロックしてしまうので、RxJavaの Single
, Completable
でラップして使いました。
// query val source: SingleOnSubscribe<List<Message>> = SingleOnSubscribe { val listener = object : ResultListener<GraphQLResponse<Iterable<Message>>> { override fun onResult(response: GraphQLResponse<Iterable<Message>>) { val messages: List<Message> = response?.data?.map { it }.orEmpty() it.onSuccess(messages) } override fun onError(e: Throwable) { } } Amplify.API.query(request, listener) } return Single.create(source) // mutation return Completable.create { emitter -> Amplify.API.mutate( request, object : ResultListener<GraphQLResponse<Message>> { override fun onResult(response: GraphQLResponse<Message>) { response.data?.let { emitter.onComplete() } } override fun onError(e: Throwable) { emitter.onError(e) } }) } // subscription return Single.create { emitter -> Amplify.API.subscribe( Message::class.java, SubscriptionType.ON_CREATE, object : StreamListener<GraphQLResponse<Message>> { override fun onNext(response: GraphQLResponse<Message>) { response?.data?.let { emitter.onSuccess(it) } } override fun onComplete() { } override fun onError(e: Throwable) { emitter.onError(e) } } ) }
利用する側は次の通り subscribeOn(Schedulers.io())
とすればワーカスレッドで処理が実行されます。
compositeDisposable.add(
mutation(
name,
editTextContent.text.toString(),
DEFAULT_ROOM
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onComplete = {
editTextContent.setText("")
},
onError = {
Log.e(TAG, it.message, it)
}
)
)
APIとしてListenerを受け取る以上、設計としては非同期なコールバックとして意図されている気がします。この辺りは折角のオープンソースなので何らかのフィードバックを行いたいと思います。
それから、前回のエントリ同様に、AWSMobileClient
を使うことで認証機能を簡単に追加することができますが、認証にAWSのCognito User Poolsを使った場合Subscriptionがうまく動かないようです。SubscriptionAuthorizationHeader.from
の実装を見る限り、まだAPI Keyにしか対応していないのかも知れません。この辺り、やはりまだまだプロダクション・レディとは言えません。
まとめ
ということで、駆け足になりましたが、今月プレビューリリースされたばかりのAmplify for Androidをざっと触ってみた感じをご紹介しました。 ライブラリとしてはまだまだこれから成熟していくのに期待しますが、AmplifyはたとえばAppSyncのコンソールで非常に簡単にクエリを実行して確認できるなど、デベロッパー体験が非常によいです。Amplifyは正直もっと流行っていいと思っています。これからも情報発信してゆくのでどうかお楽しみに。
それではよいお年を。