API Interface 有非常多種,除了常見的 REST API,還有 WebSocket, gRPC, (底層的) DLL Interop, Protobuf, TCP Sockers, UDP, WebTransport, GraphQL...etc ,GraphQL 就是其中一種通訊界面,在 REST API 中,是透過 Request, Response Body 來解析資料,在 GraphQL 中是提早定義好要呼叫的 3 種型態: Query, Mutation, Subscription ,呼叫方式如果帶有 Params,就會放在呼叫 Endpoint 的 Params 中,然後嚴格要求 Server 按照 GraphQL Schema 定義給出資料,否則就會出錯,因此在 GraphQL 框架這端,就幫忙做到了驗證。
本篇並不會細說 GraphQL 真正使用方式,而是要記錄如何使用 Elixir Build 一個 GraphQL Project。
Where's the Pattern?
GraphQL 與 Phoenix 和 Ecto 的關係,基本上,Phoenix 提供整套建置方案,可以從建 Schema,到查詢資料,都用 Phoenix 指令幫你產生 Ecto 的物件們,GraphQL 的 Absinthe 則是繼承在 Phoenix API 上的一個 application。
一如往常,只需要一個連入的 api endpoint,像是: /query。
從純 Phoenix 專案建立 GraphQL
有三件事要做,讓 Phoenix 直接變成 GraphQL:
1. mix.ex 中,加入 Absinthe 套件
defp deps do [ {:absinthe, "~> 1.6"}, {:absinthe_plug, "~> 1.5"}, {:jason, "~> 1.1"},
2. 建立 Schema.ex
Absinthe 的 GraphQL Schema 不需要自己手寫 GraphQL ,而是透過 Elixir 本身的 meta-programming 去實現,Absinthe 也會自動幫你產生 GraphQL Schema。
這個 Schema 預設沒有,要在 /drent/lib/drent_web/ 下建立一個檔案: /drent/lib/drent_web/schema.ex。
# 定義這個 Schema 模組 defmodule DrentWeb.Schema do # 使用 Absinthe.Schema 所有的內容 use Absinthe.Schema # 定義 GraphQL Object Struct 的型態 # non_null 就是 !,像是 id: Int! # non_null(list_of(non_null(:string))) 就是: [String!]! object :staff do field :id, non_null(:id) field :fullname, non_null(:string) end # 定義 Query 所有的 Resolvers query do end # 定義 Mutation 所有的 Resolvers mutation do end end
3. API 加上 GraphQL Entry Point
在 /drent/lib/drent_web/router.ex 中,直接新建某個 scope,然後給予 GraphQL 進入點:
scope "/" do pipe_through :api forward "/graphiql", Absinthe.Plug.GraphiQL, schema: DrentWeb.Schema, # Schema 模組 (defmodule) 確切位置 interface: :simple, # 簡易模式,可以改 :advenced context: %{pubsub: DrentWeb.Endpoint} end
這麼一加完,在指令處執行:
mix phx.server
打開: localhost:4000/graphiql (注意是 i ql),就會看到 gql 操作介面。
建立 Schema
剛才的 Schema 中的 object 就是每一個單結構的定義,如果要新增兩個不同的 Struct ,則可以這麼寫:
object :staff do field :id, non_null(:id) # gql: id ID! field :fullname, non_null(:string) # gql: fullname String! end object :test do field :testname, :string # gql: testname String (可為空) end
建立 Query, Mutation Resolvers
就只用來操作 Staff 的 CRUD,直接在 /drent/lib/drent_web/ 新增一個 resolvers 資料夾,變成: /drent/lib/drrent_web/resolvers,然後底下新增一個檔案叫做 StaffResolver.ex,裡面是:
# 定義 Resolver 的模組路徑 defmodule DrentWeb.StaffResolver do alias Drent.Users # 取得所有 staff def all_staff(_root, _args, _info) do # phoneix 指令是建立在 User 下, staff, profile 的東西都在 Users 裡,故 alias Drent.Users # 而不是 alias Drent.Users.Staff {:ok, Users.list_staffs()} end # 取得單一 staff # 假設傳進來的 args 是: %{ id: 1 } def get_staff(_root, args, _info) do # 直接開啟 case pattern-match 決定要回傳什麼 case Users.get_staff!(args.id) do nil -> {:error, "EMPTY"} staff -> {:ok, staff} end end # 刪除 staff def remove_staff(_root, args, _info) do Users.delete_staff(%Users.Staff{ id: args.id }) {:true} end # 更新 staff 的名字 (用上一集寫過的 ecto) def rename_staff(_root, args, _info) do Users.rename_staff_by_id(args.id, args.fullname) {:true} end end
在這邊,在最後的 rename_staff 還沒有新增過 Users 有這個功能,於是複製上一篇最後一個 rename 功能,在 /drent/lib/drent/users.ex 這個外部的 檔案,結尾處內加入這個 def function:
def rename_staff_by_id(%Staff{} = staff, _atttrs \\ %{}) do Repo.update(Ecto.Changeset.cast( %Staff{ id: staff.id }, %{ "fullname" => staff.fullname }, [ :fullname ] )) end
這麼一來, StaffResolver.ex 就可以使用這個功能了。
最後,我們需要新增這些 Resolver 操作到 schema.ex 中:
# 定義這個 Schema 模組 defmodule DrentWeb.Schema do # 使用 Absinthe.Schema 所有的內容 use Absinthe.Schema @desc """ 這像是: type Staff { id: ID! fullname: string! } """ object :staff do field :id, non_null(:id) field :fullname, non_null(:string) end # 引用其他 Resolver 模組 alias DrentWeb.StaffResolver # 定義 Query 所有的 Resolvers query do @desc """ Get all staffs 這像是: type Query{ query get_all_staff(): [Staff!]! } """ field :get_all_staff, non_null(list_of(non_null(:staff))) do # 回傳這個 Resolver 裡面的某個 Function 回去,這是回傳 Function 本身 # 是給別人去執行這個 Function 的 Link,通常後面 /3 是指要找多少參數的 Func resolve &StaffResolver.all_staff/3 end @desc """ Get specific staff by id. (注意可為 null) 這像是: type Query { query get_staff_by_id(id: ID!): Staff } 注意, arg 這個才是 func 裡面的參數, arg 也可以很多個,像是: arg :id, non_null(:id) arg :fullname, :string gql 就是 query get_staff_by_id(id: ID!, fullname: String): Staff """ field :get_staff_by_id, :staff do arg :id, non_null(:id) resolve &StaffResolver.get_staff/3 end end # 定義 Mutation 所有的 Resolvers mutation do @desc """ Remove Specific Staff by id gql: type Mutation { remove_staff_by_id(id: ID!): Boolean! } """ field :remove_staff_by_id, non_null(:boolean) do arg :id, non_null(:id) resolve &StaffResolver.remove_staff/3 end @desc """ Update Specific Staff Name gql: type Mutation { rename_staff_by_id(id: ID!, fullname: String!): Boolean! } """ field :rename_staff_by_id, non_null(:boolean) do arg :id, non_null(:id) arg :fullname, non_null(:string) resolve &StaffResolver.rename_staff/3 end end end
{ getAllStaff{ id fullname } }
GraphQL 操作測試 - 單一 Staff:
# 這裡的 $id 是從 query variables 的 key 來的 query queryStaff($id: ID!){ # 這裡放的是從 query variables 帶來的東西 getStaffById(id: $id){ id fullname } }
{ "id": 1 }
mutation renameStaffById($id: ID!, $fullname: String!){ renameStaffById(id: $id, fullname: $fullname) }
{"id": 1, "fullname": "FOX V3"}
mutation removeStaff($id: ID!){ removeStaffById(id: $id) }
{"id": 1}
References:
https://cloud-trends.medium.com/grpc-vs-restful-api-vs-graphql-web-socket-tcp-sockets-and-udp-beyond-client-server-43338eb02e37
https://s.itho.me/cloudsummit/2020/slides/7034.pdf
https://www.howtographql.com/graphql-elixir/1-getting-started/
沒有留言:
張貼留言