どうも、ぱか@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]
を追記します。
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対策をしないといけません。
今回は適当に、コメントアウトで済ませます。
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
を再読込してもう一度見てみます。
わーい!!! 成功です。
ここまでで準備段階のRESTサーバーをつくりました。
普段のRESTサーバーであれば、ここで終了です。
今回はGraphQLですので、ここからAbsintheを試していきましょう。
Absintheの導入
例のごとくmix.exsに追記します。
def deps do
[
# ...
{:absinthe, "~> 1.4"},
{:absinthe_plug, "~> 1.4"},
]
end
下記コマンドでパッケージをgetします。
mix deps.get
Ecto側のスキーム(下記コード)を見比べながら、Absintheのスキームを作っていきます。
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等を試すのに便利です。
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 を開いてみます。すると、以下のようなウェブインターフェースがでてきます。
Schemaの作成
まず、lib/phx_vue_web/schema.ex
という新しいexファイルを作ります。
ここに、どのようにAbsintheが振る舞うかを記述していきます。
基本的に最低限やるべきことは以下の3つです。
- objectの定義
- queryとmutationの定義
- resolverの定義
順に説明していきます。
Objectの定義
GraphQLが扱うobjectを定義していきます。
だいぶEctoのschemaに似せてありますね。
Ecto.Schemaが対データベースであるならば、Absinthe.Schemaは対GraphQLであると考えるとラクです。
ただし、Arrayの記法のように微妙に書き方が違うので注意が必要です。
Ecto: {:array, 型}
Absinthe: list_of(型)
では、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制約をつけることもできます。つけちゃいます。
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
を作ってみます。
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をクリックしてみると、以下のようなドキュメントが出てきます。
では、左ペインに以下を書いてみて、▶を押下してリクエストしてみます。
{
allNotes{
id
body
}
}
すると、以下のような返りが得られるでしょう。
tags を追加してみます。
{
allNotes{
id
body
tags
}
}
このqueryを複雑化するといろいろなことができます。
Queryの定義②argの追加
先程のschema.ex
を編集して、idからnoteを取得できるようにしてみます。
ここでarg マクロを使います。いくつあっても大丈夫です。説明のために一つのみ使います。
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を取得できます。
わざと存在しないidを探してみましょう。
やる気のない適当なエラーメッセージが返ってきます(汗)
たとえば、tagで検索するのも同様にして作ることができます。
Mutationの定義
次は、mutationをやってみましょう。
mutationとqueryの違い(私的理解)
- queryでは、状態が変更しない操作
- mutationでは、データーベース等の状態変更にかかわる操作
先程のschema.ex
を編集して、mutationを作ってみましょう。
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
}
}
一回のリクエストで、複数の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
}
}
まとめ
以上、Absinthe の導入でした。
まだ日本語情報が少ないですが、わりとサクッと導入できます。
すこしでも皆さんのお役に立てれば幸いです。
書いていて、gRPCにどことなく似た印象を持ちました。バックエンドの関数をそのまま呼び出しているような気分になります。
RESTとの共存もできますので、お試しあれ。
Vueとの組み合わせもいい具合にできます。
次は、EctoのDataloaderについてまとめようかと思います(こちらも日本語情報が少ないですので。)。
VuejsでApollo等をつかってどのように連携させるかはそのうち書く(かもしれない)。
参考