10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-08

どうも、ぱか@u3pakaです。
2019年もよろしくお願いいたします。

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

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

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

おしながき

ぱかのアトリエ〜elixirの錬金術士〜第2回
|> GraphQLとは
|> Absintheのプロジェクト作成
|> JSON-RESTサーバーの作成
|> Absintheの導入
|> エンドポイントとGraphiQLの追加
|> Schemaの作成
|> Objectの定義
|> Queryの定義①resolverへの橋渡し
|> Queryの定義②argの追加
|> Mutationの定義
|> まとめ

GraphQLとは

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

公式を覗いたほうがよさそう。また、先人方の解説もある。
https://graphql.org

GET POST PUT DELETE などのおなじみの動詞ではなく、以下のようなQueryでリクエストします。

{
  allNotes {
    id
    body
    tags
  }
}

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

{
  "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]を追記します。

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対策をしないといけません。
今回は適当に、コメントアウトで済ませます。

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

わーい!!! 成功です。

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

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

Absintheの導入

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

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

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

mix deps.get

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

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等を試すのに便利です。

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

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に書いていきます。

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の以下に相当します。

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

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

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では、!(エクスクラメーションマーク)です。

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

Queryの定義①resolverへの橋渡し

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

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

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

{
  allNotes{
    id
    body
  }
}

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

image.png

tags を追加してみます。

{
  allNotes{
    id
    body
    tags
  }
}

image.png

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

Queryの定義②argの追加

先程のschema.exを編集して、idからnoteを取得できるようにしてみます。

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

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

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

image.png

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

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

Mutationの定義

次は、mutationをやってみましょう。

mutationとqueryの違い(私的理解)

  • queryでは、状態が変更しない操作
  • mutationでは、データーベース等の状態変更にかかわる操作

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

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の書き方は以下のようなものです。

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

以下のような結果が得られると思います。
image.png

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

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

まとめ

以上、Absinthe の導入でした。

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

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

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

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

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

参考

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?