2021年5月25日 星期二

Building GraphQL pattern with Ecto (II) - GraphQL Part


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 中:


# 定義 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


  # 轉 type 用模組
  alias Integer


  # 刪除 staff
  def remove_staff(_root, args, _info) do
    # args.id 是 ID 被認成 string,因此強制轉為 Int
    { id, _ } = Integer.parse( args.id )

    Users.delete_staff(%Users.Staff{ id: id })
    # 這是 absinthe return resolver 的格式
    {:ok, true}
  end


  # 更新 staff 的名字 (用上一集寫過的 ecto)
  def rename_staff(_root, args, _info) do
    # args.id 是 ID 被認成 string,因此強制轉為 Int
    { id, _ } = Integer.parse( args.id )

    # 送進去應該是結構,不是單一參數
    Users.rename_staff_by_id(%Users.Staff{ id: id, fullname: args.fullname})
    {:ok, true}
  end

end




GraphQL 操作測試 - 所有 Staff:
{
  getAllStaff{
    id
    fullname
  }
}


GraphQL 操作測試 - 單一 Staff:

# 這裡的 $id 是從 query variables 的 key 來的
query queryStaff($id: ID!){
  
  # 這裡放的是從 query variables 帶來的東西
  getStaffById(id: $id){
    id
    fullname
  }
}

在 query variables 打開,放入:
{
  "id": 1
}




GraphQL 操作測試 - 更名 Staff:
mutation renameStaffById($id: ID!, $fullname: String!){
  renameStaffById(id: $id, fullname: $fullname)
}
在 query variables 打開,放入:

{"id": 1, "fullname": "FOX V3"}



GraphQL 操作測試 - 刪除 Staff:
mutation removeStaff($id: ID!){
  removeStaffById(id: $id)
}
在 query variables 打開,放入:
{"id": 1}

刪不掉可能是正常的,因為 Staff 有被其他表 Constraint ,所以可能刪不掉。


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



沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014