魔酒Absintheを飲んでみる。Phoenix + AbsintheではじめるGraphQL

どうも、ぱか[@u3paka](https://twitter.com/u3paka?lang=ja)です。
2019年もよろしくお願いいたします。

年末年始はお酒漬けな方も多いのでは？肝臓に負担がかかる日々です...

たまには趣向を変えてハーブ酒もいかがでしょうか。
今回はかつて魔酒と呼ばれたアブサンに由来する[GraphQL](https://graphql.org)用のパッケージ[Absinthe](https://absinthe-graphql.org) を紹介します。

相変わらず厨二病ネーミングセンスで、好きです。

# おしながき
ぱかのアトリエ〜elixirの錬金術士〜第2回
|> [GraphQLとは](#graphqlとは)
|> [Absintheのプロジェクト作成](#absintheのプロジェクト作成)
|> [JSON-RESTサーバーの作成](#json-restサーバーの作成)
|> [Absintheの導入](#absintheの導入)
|> [エンドポイントとGraphiQLの追加](#エンドポイントとgraphiqlの追加)
|> [Schemaの作成](#schemaの作成)
|> [Objectの定義](#objectの定義)
|> [Queryの定義①resolverへの橋渡し](#queryの定義resolverへの橋渡し)
|> [Queryの定義②argの追加](#queryの定義argの追加)
|> [Mutationの定義](#mutationの定義)
|> [まとめ](#まとめ)

# GraphQLとは
Facebookが主導しているポストREST的な位置づけの模様。

公式を覗いたほうがよさそう。また、先人方の解説もある。
https://graphql.org
 
GET POST PUT DELETE などのおなじみの動詞ではなく、以下のようなQueryでリクエストします。

```graphql
{
  allNotes {
    id
    body
    tags
  }
}
```

これは、allNotesというサーバー側の操作を要求し、以下のようなレスポンスが返ってきます。

```json
{
  "data": {
    "allNotes": [
      {
        "body": "わーーい！！",
        "id": "1",
        "tags": [
          "draft",
          "test"
        ]
      },
      {
        "body": "かしこま！！",
        "id": "3",
        "tags": [
          "laala",
          "draft"
        ]
      },
      {
        "body": "elixirはいいぞ",
        "id": "4",
        "tags": [
          "布教",
          "draft"
        ]
      },
      {
        "body": "phoenixはいいぞ",
        "id": "5",
        "tags": [
          "布教",
          "draft"
        ]
      },
      {
        "body": "fukuoka.exはいいぞ",
        "id": "6",
        "tags": [
          "布教",
          "draft"
        ]
      }
    ]
  }
}
```

あまり詳しくないので、メリット・デメリットとか語れない...すみません:D

# JSON-RESTサーバーの作成
前回のVueプロジェクトに追記していきましょう。

導入からプロジェクトの雛形の作成まではこちらをどうぞ。
https://qiita.com/u3paka/items/ac3cb9ffa366ddc0ed10

今回は、タグ付きのノートを保存していく簡単な機能を追加してみます。

まず、プロジェクトに以下のようなモデルを加えていきます。
コンテクスト名は、Postとでもしておきましょう。

```
# 基本の使いかたを確認！
# mix phx.gen.json コンテクスト名 モデル名 モデル名複数形 要素1:型 要素2:型 要素3:型 ...
# 要素名:array:stringで、["memo", "note", "elixir"] のようなArrayも定義できる。

# 以下を実行
mix phx.gen.json Post Note notes body:string tags:array:string
```

結果は次のようになる。

```結果
Add the resource to your :api scope in lib/phx_vue_web/router.ex:

    resources "/notes", NoteController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

指示通りに```resources "/notes", NoteController, except: [:new, :edit]```を追記します。

```elixir:lib/phx_vue_web/router.ex
scope "/", PhxVueWeb do
    pipe_through :browser

    get "/", PageController, :index
    # ↓追加
    resources "/notes", NoteController, except: [:new, :edit]
  end
```

```
# データベースをマイグレートします。
mix ecto.migrate

# サーバー起動
mix phx.server
```

http://localhost:4000/notes
をブラウザで覗くと、jsonが返ってきます！なんて簡単なんでしょう。

POSTもやってみましょう。CSRF対策をしないといけません。
今回は適当に、コメントアウトで済ませます。

```elixir:lib/phx_vue_web/router.ex
defmodule PhxVueWeb.Router do
  use PhxVueWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    # ここをコメントアウトしておきます。
    # plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
```

クライアントは何でもOKですが、今回はChrome拡張のPostmanを使ってみます。

送信したら、http://localhost:4000/notes
を再読込してもう一度見てみます。

![post_success.png](https://qiita-image-store.s3.amazonaws.com/0/318136/c3a60799-5e2e-abe4-fc09-7110658e2159.png)

わーい！！！　成功です。

ここまでで準備段階のRESTサーバーをつくりました。
普段のRESTサーバーであれば、ここで終了です。


今回はGraphQLですので、ここからAbsintheを試していきましょう。

# Absintheの導入

例のごとくmix.exsに追記します。

```elixir:mix.exs
def deps do
  [
    # ...
    {:absinthe, "~> 1.4"},
    {:absinthe_plug, "~> 1.4"},
  ]
end
```

下記コマンドでパッケージをgetします。

```
mix deps.get
```

Ecto側のスキーム（下記コード）を見比べながら、Absintheのスキームを作っていきます。

```elixir:lib/phx_vue/post/note.ex
defmodule PhxVue.Post.Note do
  use Ecto.Schema
  import Ecto.Changeset

  schema "notes" do
    field :body, :string
    field :tags, {:array, :string}  # <- EctoでArrayを定義するときは、このように書く！
    timestamps()
  end
 
  # ...
end
```

今回は単純にbodyとtagsだけです。

## エンドポイントとGraphiQLの追加

おっと、エンドポイントとGraphiQLを追加し忘れていました。

これは、手軽なGraphQLのウェブクライアントで、query等を試すのに便利です。

```elixir:lib/phx_vue_web/router.ex
defmodule PhxVueWeb.Router do
  # ...

  # scope "/api", PhxVueWeb do ... だと、スコープ設定してしまい、Absintheが読み込めない。
  # 以下に書き換える。 参考：https://stackoverflow.com/questions/45765950/absinthe-plug-not-available
  scope "/api" do
    pipe_through :api
    
    forward "/graphiql", Absinthe.Plug.GraphiQL,
      schema: PhxVueWeb.Schema,
      json_codec: Jason,
      interface: :simple,
      context: %{pubsub: PhxVueWeb.Endpoint}

    forward "/", Absinthe.Plug, schema: PhxVueWeb.Schema, json_codec: Jason
  end
end
```

Absinthe標準のJSON取扱いパッケージはPoisonです。もし、Jasonなど別のものを使う場合は

```
json_codec: Jason
```
をforwardのオプションに追記します。今回はJasonを使っていきます。

http://localhost:4000/api/graphiql を開いてみます。すると、以下のようなウェブインターフェースがでてきます。

![graphiql_startup.png](https://qiita-image-store.s3.amazonaws.com/0/318136/4e3eef6e-b198-785c-139c-2f343bb25e5e.png)

# Schemaの作成
まず、`lib/phx_vue_web/schema.ex`　という新しいexファイルを作ります。

ここに、どのようにAbsintheが振る舞うかを記述していきます。
基本的に最低限やるべきことは以下の3つです。

1. objectの定義
2. queryとmutationの定義
3. resolverの定義

順に説明していきます。

## Objectの定義

GraphQLが扱うobjectを定義していきます。

だいぶEctoのschemaに似せてありますね。

Ecto.Schemaが対データベースであるならば、Absinthe.Schemaは対GraphQLであると考えるとラクです。

ただし、Arrayの記法のように微妙に書き方が違うので注意が必要です。

```
Ecto: {:array, 型}
Absinthe: list_of(型)
```

では、`lib/phx_vue_web/schema.ex`に書いていきます。

```elixir:lib/phx_vue_web/schema.ex

defmodule PhxVueWeb.Schema do
  use Absinthe.Schema

  # objectマクロで定義
  object :note do
    field :id, :id
    field :body, :string
    field :tags, list_of(:string)
  end
end
```

これは、GraphQLの以下に相当します。

```graphql
type Note {
  id: ID
  body: String
  tags: [String]
}
```

もちろん、null制約をつけることもできます。つけちゃいます。

```elixir:lib/phx_vue_web/schema.ex

defmodule PhxVueWeb.Schema do
  use Absinthe.Schema

  # objectマクロでNoteを定義
  object :note do
    field :id, non_null(:id)
    field :body, non_null(:string)
    field :tags, non_null(list_of(non_null(:string)))
  end
end
```

GraphQLでは、!（エクスクラメーションマーク）です。

```graphql
type Note {
  id: ID!
  body: String!
  tags: [String!]!
}
```

## Queryの定義①resolverへの橋渡し

では、すべてのNoteを取得するクエリ`allNotes`を作ってみます。

```elixir:lib/phx_vue_web/schema.ex

defmodule PhxVueWeb.Schema do
  use Absinthe.Schema

  # objectマクロでNoteを定義
  object :note do
    field :id, non_null(:id)
    field :body, non_null(:string)
    field :tags, non_null(list_of(:string))
  end
  
  # queryマクロ
  query do
    @desc "Get all notes"
    field :all_notes, list_of(:note) do
      resolve(fn _parent, _args, _resolution ->
        {:ok, Repo.all(Note)}
      end)
    end
  end
end
```

resolveには、アリティ3の`{:ok, return_object}`あるいは`{:error, error_value}`を返す関数を与えてあげればOKです。

基本的には、schemaがやっていることは、resolverへの橋渡しだと考えるとわかりやすいと思います。

field名はスネークケースのatomで書くと自動で変換してくれます。
e.g. :all_notes -> allNotes


http://localhost:4000/api/graphiql で確認してみましょう。

右端のDocsからRootQueryTypeをクリックしてみると、以下のようなドキュメントが出てきます。
![graphiql_docs.png](https://qiita-image-store.s3.amazonaws.com/0/318136/208bb2e8-8c03-3ed4-4dd8-bafd4b3b24ef.png)

では、左ペインに以下を書いてみて、▶を押下してリクエストしてみます。

```
{
  allNotes{
    id
    body
  }
}
```

すると、以下のような返りが得られるでしょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/8f3fd3cd-4454-42d1-22f6-30bc45068fe3.png)

tags　を追加してみます。

```
{
  allNotes{
    id
    body
    tags
  }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/dfd667ad-30cd-c950-cade-4948c435d11c.png)

このqueryを複雑化するといろいろなことができます。

## Queryの定義②argの追加
先程の`schema.ex`を編集して、idからnoteを取得できるようにしてみます。

ここでarg マクロを使います。いくつあっても大丈夫です。説明のために一つのみ使います。

```elixir:lib/phx_vue_web/schema.ex

defmodule PhxVueWeb.Schema do
  use Absinthe.Schema
  # ...
  # queryマクロ
  query do
    @desc "Get all notes"
    field :all_notes, list_of(:note) do
      resolve(fn _parent, _args, _resolution ->
        {:ok, Repo.all(Note)}
      end)
    end
   # 以下を追加。
    @desc "Get note by id"
    field :note, :note do
      arg(:id, :id)
      # ↓ resolverの第二引数にmapとしてarg macroが渡されるので、パターンマッチ。
      resolve(fn _parent, %{:id => note_id}, _resolution ->
        Note
        |> Repo.get(note_id)
        |> case do
          nil -> {:error, "ないよ"}
          x -> {:ok, x}
        end
      end)
  end
end
```


arg は、resolverの第二引数にmapとして渡されるので、パターンマッチする。
Resolverは個別にモジュールを作って分離させてあげるのが本来は吉です。

これによって、以下のようなクエリが使えます。

```
{
  note(id:1){
    body
    tags
  }
}
```

idが1のNoteを取得できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/73befbc9-c7b4-a0a4-c2d8-759ae92a074c.png)

わざと存在しないidを探してみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/687cc701-c5f3-84ba-9ed2-8c4177fdb73c.png)

やる気のない適当なエラーメッセージが返ってきます（汗）

たとえば、tagで検索するのも同様にして作ることができます。

# Mutationの定義

次は、mutationをやってみましょう。
#### mutationとqueryの違い（私的理解）
- queryでは、状態が変更しない操作
- mutationでは、データーベース等の状態変更にかかわる操作

先程の`schema.ex`を編集して、mutationを作ってみましょう。

```elixir:lib/phx_vue_web/schema.ex
defmodule PhxVueWeb.Schema do
  use Absinthe.Schema

  alias PhxVue.Repo
  alias PhxVue.Post.{Note}
# ...
  query do
   # ...
  end

  # queryの下に、mutation追加
  mutation do
    @desc "create new note"
    field :create_note, :note do
      arg(:body, non_null(:string))
      arg(:tags, list_of(:string))

      resolve(fn _parent, args, _resolution ->
        %Note{}
        |> Note.changeset(args)
        |> Repo.insert()
        |> case do
          {:ok, note} -> {:ok, note}
          _error -> {:error, "could not create a note"}
        end
      end)
    end
  end
end
```
mutationもfield やarg、resolverの書き方はqueryと同じです。

Graphiql で試してみましょう。

mutationの書き方は以下のようなものです。

```graphql
mutation{
  createNote(body: "かしこま！！", tags:["laala", "draft"]){
    id
    body
    tags
  }
}
```

以下のような結果が得られると思います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/f0d6ccbd-40a6-29e0-b0e4-bfb1346f8fdb.png)


一回のリクエストで、複数のmutationを送信することもできます！

```graphql
mutation{
  elixir: createNote(body: "elixirはいいぞ", tags:["布教", "draft"]){
    id
    body
    tags
  }
  
  phoenix: createNote(body: "phoenixはいいぞ", tags:["布教", "draft"]){
    id
    body
    tags
  }
  
  fukuokaex: createNote(body: "fukuoka.exはいいぞ", tags:["布教", "draft"]){
    id
    body
    tags
  }
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/318136/f4d4678b-ded3-1989-2bf2-1e6b7f6f4cbc.png)

# まとめ
以上、Absinthe の導入でした。

まだ日本語情報が少ないですが、わりとサクッと導入できます。

すこしでも皆さんのお役に立てれば幸いです。

書いていて、gRPCにどことなく似た印象を持ちました。バックエンドの関数をそのまま呼び出しているような気分になります。

RESTとの共存もできますので、お試しあれ。
Vueとの組み合わせもいい具合にできます。

次は、EctoのDataloaderについてまとめようかと思います（こちらも日本語情報が少ないですので。）。
VuejsでApollo等をつかってどのように連携させるかはそのうち書く（かもしれない）。


# 参考

https://qiita.com/hykw/items/96ef2128902508b0c7ef

https://qiita.com/getty104/items/79bfb32239dc464f7650

https://qiita.com/ma2ge/items/52dcebd54c5bdecafcb6

https://hexdocs.pm/absinthe/overview.html

https://graphql.org/learn/schema/

https://www.howtographql.com/graphql-elixir

https://qiita.com/u3paka/items/ac3cb9ffa366ddc0ed10