Amplify DataStoreを使ったチャットアプリサンプル

今回は、前から気になっていたAmplify DataStoreを使ったWebアプリを作ってみます。
[Amplify DataStore](https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js)はデータの書き込み、読み取り、監視するための永続的なストレージリポジトリです。ネットワーク接続が利用可能であればデータをAppSyncと同期し、接続しない場合はローカルデータストアとしても利用ができます。

#Reactチャットアプリ雛形作成
```
npx create-react-app chatapp --template typescript
cd chatapp
yarn start
```
ブラウザにいつものが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/16d45078-d2ba-8c40-df3a-547449be4116.png)

とりあえずApp.tsxの不要なものを消しておきます。

```App.tsx
import React from 'react';
import './App.css';

function App() {
  return (
    <>
      ここに後でchatアプリ作るよ
    </>
  );
}

export default App;
```
とりあえず表示はこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/06413f16-d4f6-4a5e-5cac-3b49aa3de4f2.png)


#Amplify DataStore設定
次にamplify CLIを使ってapiを作って行きますが、その前にamplifyはバージョン更新が激しいので古いバージョンのCLIを使っていると利用したいオプションがサポートされていないこともあるので、作業の前に念の為ご自身の利用しているCLIをバージョンを確認しておきます。私が使ったのは以下のバージョン。

```
amplify --version
4.27.2
```

では、まずは初期化から。

```
amplify init
```

ここからが本題。DataStoreを使うAPIを作ります。今回はサンプルなのでほぼデフォルト設定ですが、以下の項目を指定する点がポイントです。

Do you want to configure advanced settings for the GraphQL API
→Yes, I want to make some additional changes.
Configure conflict detection?
→Yes
Select the default resolution strategy
→Auto Merge 

```
amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: chatapp
? Choose the default authorization type for the API API key
? Enter a description for the API key: 
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API Yes, I want to make s
ome additional changes.
? Configure additional auth types? No
? Configure conflict detection? Yes
? Select the default resolution strategy Auto Merge
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, des
cription)
? Do you want to edit the schema now? Yes
```
AppSyncとの同期時の競合検出と解決については以下３つの動作がサポートされています。

```
 Auto Merge 
 Optimistic Concurrency 
 Custom Lambda 
```
###Auto Merge
データの競合を検出すると自動でデータをマージし、クラウド上とクライアントのローカルストレージ上のデータを更新します。デフォルトは「Auto Merge」が選択されています。
###Optimistic Concurrency
データの競合を検出すると、クライアントからのリクエストを拒否します。
###Custom Lambda
データの競合を検出すると、独自定義したLambdaを起動させることができます。

特に理由がなければAuto Mergeを選択して良いと思います。

詳しく知りたい方は[公式](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html)の「Conflict Detection and Resolution」章あたりを参照すると良いでしょう。

スキーマはシンプルに以下のようにします。（説明簡単にするため相当雑に作ってます...）

```schema.graphql
type Message @model {
  id: ID!
  text: String!
}
```

ファイルを保存してバックエンドへプロビジョニングします。

```
amplify push
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API No
```

次に、以下のオプションを実行するとDataStoreのmodelが生成されます。

```
amplify codegen models
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/6b7d5522-00d6-cdca-9236-60a958382a8e.png)

#チャットアプリへDataStoreを組み込む
必要なライブラリを追加します。
```
yarn add aws-amplify @aws-amplify/datastore
```
App.tsxに実装を加えます。

```App.tsx
import React, { useState, useEffect } from 'react';
import './App.css';
import awsconfig from './aws-exports'
import Amplify, { DataStore } from 'aws-amplify'
import { Message } from './models';
Amplify.configure(awsconfig)

const App = () => {
  const [messages, setMessages] = useState<Message[]>([])
  const [inputValue, setInputValue] = useState('')
  useEffect(() => {
    fetchMessage()
    const subscription = DataStore.observe(Message).subscribe(fetchMessage)
    return () => {
          subscription.unsubscribe()
    }
  }, [])
  async function fetchMessage() {
    const data = await DataStore.query(Message)
    setMessages(data)
  }
  async function sendMessage(text: string) {
    await DataStore.save(new Message({ text }))
    setInputValue('')
  }
  const handleOnChange = (value: string) => {
    setInputValue(value)
  }
  return (
    <>
      {messages && messages.map((message, index) => {
        return (
          <div key={index}>
            {message.text}
          </div>
        )
      })}
      <input type='text' value={inputValue} onChange={(e) => handleOnChange(e.target.value)} />
      <button onClick={() => sendMessage(inputValue)}>送信</button>
    </>
  );
}

export default App;

```

表示はこんな感じ。ちょーシンプル。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/655d29d2-7fda-420a-9d28-0fb27c891231.png)

試しに複数ブラウザからアクセスし一方をオフラインにしてメッセージを送信してみましょう。オンライン復帰後メッセージが自動的にマージされることが確認できるかと思います。

#その他の操作
今回のサンプルでは簡単なquery、save、subscriptionを例示しましたが、その他の操作についても載せておきます。

##条件付きのQuery

```query.ts
  const fetchMessage = async (id: string) => {
    const data = await DataStore.query(Message, m => m.id('eq', id))
    //do something

  }
```
##更新
```save.ts
  const updateMessage = async (message: Message, text: string) => {
    await DataStore.save(Message.copyOf(message, updated => {
           updated.text = text
     }))
    //do something

  }
```
Message.copyOfの第一引数がオリジナル、第二引数のupdatedは更新処理を書きます。
DataStoreのモデルは不変なので、レコードを更新する場合はインスタンスを直接変更せず、copyOf関数を指定して属性の更新をする必要があります。
なお、copyOfを使わずに新しいオブジェクト作ってsaveした場合には、既存レコードの更新ではなく新しいレコードが作られます。（まぁ、当たり前ですけど）


##削除

```削除.ts
const todelete = await DataStore.query(Message, "1234567");
DataStore.delete(todelete);
```
たったこれだけ。
さらに以下のように削除対象レコードの条件指定も可能。([公式](https://docs.amplify.aws/lib/datastore/data-access/q/platform/js#create-and-update)の例を拝借)

```削除.ts
const todelete = await DataStore.query(Post, "123");
DataStore.delete(todelete, post => post.status("eq", PostStatus.DRAFT));
```

#まとめ
DataStoreを使うことで非常に簡単にクラウドへのデータ同期と競合解消する実装ができました。また、GraphQLのquery文が抽象化されるためよりスッキリとした実装になります。
複雑なquery条件が必要なケースなどではまだ制約がありそうですが、簡単なアプリなら十分に利用できますね。
また機会があれば、もう少し複雑なスキーマでも試してみようかと思います。