AWS Ampliy の DataStore を使ったら AppSync のデータアクセスがめちゃくちゃ楽になった

## 概要
AWS Amplify Advent Calendar 2019、23日目は 今年のre:Inventで発表されたAmplifyのDataStoreについてです。本記事ではサンプルコードの実装にReactを用いていますが、基本的な考え方はVueなどの他のフレームワークでも同じです。

## 想定読者
- Amplify 使ったことがある
- AppSync 使ったことがある

## Amplify DataStore の概要

まずは簡単に DataStore についておさらいします。DataStore は AWS AppSync に接続するための新たなインターフェースです。DataStore は以下のような特徴を持ちます。


#### 1. クラウドへのデータ同期、及び競合検知

端末のネットワーク状況(オンライン or オフライン)を検知して、端末がオンラインになったら自動的にローカルデータをクラウドに同期し、データの競合をいい感じに解決してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100291/4bfc09dd-f4e9-4b0b-c711-f3fce0346afa.png)


#### 2. デベロッパーフレンドリーなインターフェース

デベロッパーフレンドリーなインターフェースでGraphQLに詳しくなくてもAppSyncからデータを操作できます。GraphQLの操作を抽象化した関数を組み合わせることでAppSyncに対してクエリを発行できるようになりました。


<img width="828" alt="スクリーンショット 2019-12-23 18.08.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100291/73a0c66e-c0c7-4451-e648-8e80cc551b88.png">

## クラウドへのデータ同期、及び競合検知

DataStoreを用いることで、オフライン時のアプリの開発を簡素化することができます。
DataStoreはローカルストレージとクラウドの両方にデータの書き込みを試みます。端末がオフラインの場合、リクエストを失敗させることなく、ローカルストレージにのみデータ書き込みを行い、オンライン復旧後にクラウドへのデータ同期を行います。オフライン期間にクラウド上のデータとローカルストレージのデータで差分が発生した場合は、データのマージ、データの競合検知を自動的に行います。

そしてこれらの挙動は全て隠蔽されており、**開発者はこれらの挙動について、意識をする必要はありません！！**

DataStoreでは、データの競合時の動作を以下の3つから選択できるようになっています。

- Auto Merge (デフォルト)
- Optimistic Concurrency
- Custom Lambda

##### データの競合が起こった時の挙動１(Auto Merge)

データの競合が起こった際の挙動はデフォルトではAutomergeが選択されています。
以下の例ではClienrt A、Client Bがオフライン中に同時にデータの更新を行った場合を想定します。DataStoreは互いの更新内容を確認し、自動でデータをマージしクラウド上のデータと各クライアントのローカルストレージ上のデータに対し更新を行います。

```js
// 元のデータ
{
  id: 'id_001',
  name: 'Andy',
  hobby: [],
  _version: 1,
  _lastChangedAt: 1577001382093,
  _deleted: false
}
```

```js
// Client A がオフライン中にローカルで変更したデータ
{
  id: 'id_001',
  name: 'Andy',
  hobby: [baseball],  // Client A が hobbyに baseballを追加
  _version: 2,
  _lastChangedAt: 1577001382094,
  _deleted: false
}
```

```js
// Client B がオフライン中にローカルで変更したデータ
{
  id: 'id_001',
  name: 'Jeff',     // Client B が nameを Jeffに変更
  hobby: [tennis],  // Client B が hobbyに tennisを追加
  _version: 2,
  _lastChangedAt: 1577001382095,
  _deleted: false
}
```

この場合、Client A と Client Bの更新は矛盾なく実施することができるため、以下のようなデータにマージされます。

```js
{
  id: 'id_001',
  name: 'Jeff',     // Client B の変更が反映
  hobby: [baseball, tennis],  // Client A,B の変更を反映
  _version: 2,
  _lastChangedAt: 1577001382524,
  _deleted: false
}
```

##### データの競合が起こった時の挙動２(Optimistic Concurrency)

Optimistic Concurrencyでは、データの競合を検知した際に、単純にクライアントからのリクエストを拒否します。クライアントは、再度、クラウド上のデータを正としてデータの更新処理を行う必要があります。

##### データの競合が起こった時の挙動３(Custom Lambda)

Custom Lambdaでは競合が起こった際に、独自に定義したLambda関数を起動させることができます。Custom Lambdaを用いることで、より柔軟なデータ競合時の処理を実装することが可能です。

データの競合戦略の変更はAmplify CLIから行うことができます。特に指定がなければAuto Mergeを選択するのが良いかと思われます。

<img width="743" alt="スクリーンショット 2019-12-23 20.34.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100291/1d9d7a01-bd68-5488-05e4-efb9676ac0c6.png">




## デベロッパーフレンドリーなインターフェース

### DataStoreを用いたAppSyncのデータアクセス方法

先ほども言及したように、DataStoreの登場で、デベロッパーはGraphQLを記述しなくてもAppSync経由でデータストアにアクセスできるようになりました。

例えば、以下のようなスキーマがあった場合に、

```ts
enum PostStatus {
  ACTIVE
  INACTIVE
}

type Post @model {
  id: ID!
  title: String!
  rating: Int!
  status: PostStatus!
}
```

評価(rating)が4以上の投稿を抽出するには以下のように記述することができます。

```ts
import { Post } from "./models";

const posts = await DataStore.query(Post, c => c.rating("gt", 4));
```

第一引数には、取得する対象のデータモデルのClassを指定し、第二引数には取得するデータの検索条件を指定します。
さらに複雑な条件を記述することもできます。たとえば、以下の例では、「ratingが4以上、もしくは、ステータスがACTIVEな投稿」を取得しています。

```ts
import { Post } from "./models";
const posts = await DataStore.query(Post, c => c.or(
  c => c.rating("gt", 4).status("eq", PostStatus.ACTIVE)
));
```

ちなみにこれと同じクエリをGraphQLで発行すると以下のようになります。

```ts

const posts = await API.graphql(graphqlOperation(`
  query listPosts {
    listPosts(
      filter: {
        or: {
          rating: {
            gt: 4
          }
          status: {
            eq: ACTIVE
          }
        }
      }
    ) {
      items {
        id
        title
        rating
      }
    }
  }
`));

```

どうでしょうか。このようにGraphQLに精通していなくてもAppSyncに対しクエリを発行することができます。

### 静的型付け言語の恩恵

関数でクエリの操作を行えるようになったことで、TypeScriptやKotlin、Swiftといった静的型付け言語の恩恵を受けやすくなります。今まではGraphQLのクエリを文字列で記述していたのに対し、DataStoreでは関数でクエリを組み立てていくため、IDEの補完機能を用いながら実装を行うことができるようになります。
<img width="2181" alt="スクリーンショット 2019-12-23 22.58.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100291/1fb9f9be-2c70-5f15-b787-580ea0cc6338.png">


Amplify CLI の`codegen` というコマンドを実行することで、作成したモデル定義からAppSyncへのデータアクセスの実装に必要なソースコードを自動で生成してくれます。このコマンドを用いることで、静的型付けに必要なコードも自動で出力してくれます。

先ほどのモデルを定義した状態で、`codegen`コマンドを発行すると、以下のようなコードが出力されます。

```ts
import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore";

export enum PostStatus {
  ACTIVE = "ACTIVE",
  INACTIVE = "INACTIVE"
}

export declare class Post {
  readonly id: string;
  readonly title: string;
  readonly rating: number;
  readonly status: PostStatus | keyof typeof PostStatus;
  constructor(init: ModelInit<Post>);
  static copyOf(source: Post, mutator: (draft: MutableModel<Post>) => MutableModel<Post> | void): Post;
}
```
型定義されたClassをimportすることで、先ほどのようにIDEの機能を活用しながら実装を進めることが可能です。

上記のソースはTypeScriptを用いることを前提としています。TypeScriptを用いない場合であってもある程度補完は効きますが、やはりTypeScriptの方が補完は強力です。

<img width="1021" alt="スクリーンショット 2019-12-23 22.38.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100291/0dc2e5bc-7235-54c9-736c-1738d21419de.png">

#### まとめ
いかがでしょうか。DataStoreを用いればオフライン時のアプリの実装が非常に簡単になります。また、AppSyncへのデータ操作が関数で記述できることにより、非常に効率的に開発を行うことができるようになりました。Amplifyを使って開発をされている方は、是非DataStoreの導入も検討いただけると良いと思います！

おわり