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
end3. 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/
沒有留言:
張貼留言