0
0

Goで学ぶGraphQLサーバーサイド(10)ーGraphQL特有のミドルウェア

Posted at

こんにちは。

今回は「GraphQL特有のミドルウェア」について説明していきます。

この章について

前章にて「クエリ複雑度によって、リクエストの受付可否を決める」機能を、エクステンションというミドルウェアのようなものを使って導入しました。
しかし、GraphQLには他にもリゾルバによる処理前後にロジックを挟むミドルウェアが存在します。
本章ではそれらを紹介したいと思います。

GraphQLサーバーに適用できるミドルウェア

github.com/99designs/gqlgen/graphqlの中に定義されているミドルウェアは全部で4種類存在します。

type OperationMiddleware func(ctx context.Context, next OperationHandler) ResponseHandler
type ResponseMiddleware func(ctx context.Context, next ResponseHandler) *Response
type RootFieldMiddleware func(ctx context.Context, next RootResolver) Marshaler
type FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)

ミドルウェアの導入法

これらのミドルウェアをGraphQLサーバーに導入するためには、Server構造体に用意されている以下のメソッドをそれぞれ使うことになります。

func (s *Server) AroundOperations(f graphql.OperationMiddleware)
func (s *Server) AroundResponses(f graphql.ResponseMiddleware)
func (s *Server) AroundRootFields(f graphql.RootFieldMiddleware)
func (s *Server) AroundFields(f graphql.FieldMiddleware)
server.go
func main() {
	// (中略)

	srv := handler.NewDefaultServer(internal.NewExecutableSchema(internal.Config{
		Resolvers: &graph.Resolver{
			Srv:     service,
			Loaders: graph.NewLoaders(service),
		},
		Complexity: graph.ComplexityConfig(),
	}))
+	srv.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
+		// (処理内容)
+	})
+	srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
+		// (処理内容)
+	})
+	srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
+		// (処理内容)
+	})
+	srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
+		// (処理内容)
+	})

	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
	http.Handle("/query", srv)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

各種ミドルウェアの機能

ここからは、4種類あるそれぞれのミドルウェアがどういうはたらきをするのかを紹介していきます。

OperationMiddleware

OperationMiddlewareは、クライアントからリクエストを受け取ったときに最初に呼ばれるミドルウェアです。
このミドルウェアによる処理が行われた後に、実際に送られてきたクエリを解釈するステップに入ります。

type OperationMiddleware func(ctx context.Context, next OperationHandler) ResponseHandler

利用例を以下に示します。

server.go
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
	log.Println("before OperationHandler")
	res := next(ctx)
	defer log.Println("after OperationHandler")
	return res
})
2023/12/12 23:36:46 connect to http://localhost:8080/ for GraphQL playground
2023/12/12 23:37:02 before OperationHandler
2023/12/12 23:37:02 after OperationHandler

ResponseMiddleware

ResponseMiddlewareOperationMiddlewareによる前後処理を通した後、クライアントに返すレスポンスを作成するという段階の前後処理を担います。

type ResponseMiddleware func(ctx context.Context, next ResponseHandler) *Response

利用例を以下に示します。

server.go
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
	log.Println("before OperationHandler")
	res := next(ctx)
	defer log.Println("after OperationHandler")
	return res
})
+srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
+	log.Println("before ResponseHandler")
+	res := next(ctx)
+	defer log.Println("after ResponseHandler")
+	return res
+})
2023/12/12 23:47:27 connect to http://localhost:8080/ for GraphQL playground
2023/12/12 23:47:42 before OperationHandler
2023/12/12 23:47:42 after OperationHandler
2023/12/12 23:47:42 before ResponseHandler
2023/12/12 23:47:42 after ResponseHandler

OperationMiddlewareの後処理 → ResponseMiddlewareの前処理という順になっていることが見て取れます。

RootFieldMiddleware

RootFieldMiddlewareとは、レスポンスデータ全体を作成するルートリゾルバの実行前後に処理を挿入するミドルウェアです。

type RootFieldMiddleware func(ctx context.Context, next RootResolver) Marshaler

利用例を以下に示します。

server.go
srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
	log.Println("before ResponseHandler")
	res := next(ctx)
	defer log.Println("after ResponseHandler")
	return res
})
+srv.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
+	log.Println("before RootResolver")
+	res := next(ctx)
+	defer func() {
+		var b bytes.Buffer
+		res.MarshalGQL(&b)
+		log.Println("after RootResolver", b.String())
+	}()
+	return res
+})
2023/12/13 00:02:16 connect to http://localhost:8080/ for GraphQL playground
2023/12/13 00:02:21 before ResponseHandler
2023/12/13 00:02:21 before RootResolver
2023/12/13 00:02:21 after RootResolver {"id":"PJ_1","title":"My Project","url":"http://example.com/project/1"}
2023/12/13 00:02:21 after ResponseHandler

ResponseMiddlewareとの関係は以下のようになっています。

  1. クライアントに返却するレスポンス作成前(=ResponseMiddlewareによる前処理)
  2. レスポンスを作成
  3. ルートリゾルバ実行前(=RootFieldMiddlewareによる前処理)
  4. ルートリゾルバを実行して、レスポンスに必要なデータを集める
  5. ルートリゾルバ実行後(=RootFieldMiddlewareによる後処理)
  6. ルートリゾルバの実行結果をjsonエンコードしてレスポンスデータとする
  7. レスポンス作成後(=ResponseMiddlewareによる後処理)

FieldMiddleware

GraphQLのレスポンスボディはjsonになっており、jsonにはkey-valueのセットで構成されているフィールドが数多く含まれていることはご存知の通りだと思います。
FieldMiddlewareは、まさにそのレスポンスに含めるjsonフィールドを1つ作る処理の前後にロジックを組み込むためのミドルウェアです。

type FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)

利用例を以下に示します。

server.go
srv.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
	log.Println("before RootResolver")
	res := next(ctx)
	defer func() {
		var b bytes.Buffer
		res.MarshalGQL(&b)
		log.Println("after RootResolver", b.String())
	}()
	return res
})
+srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
+	res, err = next(ctx)
+	log.Println(res)
+	return
+})

このようにFieldMiddlewareを組み込んだ後に、以下のようなリクエストを送ります。

query {
  node(id: "PJ_1") {
    id
    ... on ProjectV2 {
      title
      url
    }
  }
}

このクエリに対するレスポンスは、「nodeidtitleurl」4つのjsonフィールドを含みます。

{
  "data": {
    "node": {
      "id": "PJ_1",
      "title": "My Project",
      "url": "http://example.com/project/1"
    }
  }
}

そのため、FieldMiddlewareによる前処理・後処理のセットも4回呼ばれることになります。

2023/12/12 23:55:35 connect to http://localhost:8080/ for GraphQL playground
2023/12/12 23:55:38 before RootResolver
2023/12/12 23:55:38 before Resolver
2023/12/12 23:55:38 after Resolver &{PJ_1 My Project {http   example.com /project/1  false false   } 1 <nil> 0xc000283a40}
2023/12/12 23:55:38 before Resolver
2023/12/12 23:55:38 after Resolver PJ_1
2023/12/12 23:55:38 before Resolver
2023/12/12 23:55:38 after Resolver My Project
2023/12/12 23:55:38 before Resolver
2023/12/12 23:55:38 after Resolver {http   example.com /project/1  false false   }
2023/12/12 23:55:38 after RootResolver {"id":"PJ_1","title":"My Project","url":"http://example.com/project/1"}

また、RootFieldMiddlewareとの関係は以下のようになっています。

  1. ルートリゾルバ実行前(=RootFieldMiddlewareによる前処理)
  2. ルートリゾルバの実行
  3. フィールドを作成する前(=FieldMiddlewareによる前処理)
  4. レスポンスに必要なデータを集めて、レスポンスフィールドを作る
  5. フィールド作成後(=FieldMiddlewareによる後処理)
  6. 必要なフィールドを全て作るまで1に戻って繰り返す
  7. ルートリゾルバ実行(=RootFieldMiddlewareによる後処理)

まとめ - 各ミドルウェア間の関係

ここまでで、GraphQLに用意されている4つのミドルウェアを紹介してきました。

  • OperationMiddleware: クライアントからリクエストを受け取ったときに最初に呼ばれる
  • ResponseMiddleware: クライアントに返すレスポンスを作成するという段階の前後処理を担う
  • RootFieldMiddleware: レスポンスデータ全体を作成するルートリゾルバの実行前後に処理を挿入するミドルウェア
  • FieldMiddleware: レスポンスに含めるjsonフィールドを1つ作る処理の前後にロジックを組み込むためのミドルウェア

最後にまとめもかねて、これら4つを併用した場合にはどのような実行順になるのかを確認します。

server.go
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
	log.Println("before OperationHandler")
	res := next(ctx)
	defer log.Println("after OperationHandler")
	return res
})
srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
	log.Println("before ResponseHandler")
	res := next(ctx)
	defer log.Println("after ResponseHandler")
	return res
})
srv.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
	log.Println("before RootResolver")
	res := next(ctx)
	defer func() {
		var b bytes.Buffer
		res.MarshalGQL(&b)
		log.Println("after RootResolver", b.String())
	}()
	return res
})
srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
	log.Println("before Resolver")
	res, err = next(ctx)
	defer log.Println("after Resolver", res)
	return
})
2023/12/12 23:57:59 connect to http://localhost:8080/ for GraphQL playground
2023/12/12 23:58:02 before OperationHandler
2023/12/12 23:58:02 after OperationHandler
2023/12/12 23:58:02 before ResponseHandler
2023/12/12 23:58:02 before RootResolver
2023/12/12 23:58:02 before Resolver
2023/12/12 23:58:02 after Resolver {PJ_1 My Project http://example.com/project/1 1 <nil> 0xc000183830}
2023/12/12 23:58:02 before Resolver
2023/12/12 23:58:02 after Resolver PJ_1
2023/12/12 23:58:02 before Resolver
2023/12/12 23:58:02 after Resolver My Project
2023/12/12 23:58:02 before Resolver
2023/12/12 23:58:02 after Resolver http://example.com/project/1
2023/12/12 23:58:02 after RootResolver {"id":"PJ_1","title":"My Project","url":"http://example.com/project/1"}
2023/12/12 23:58:02 after ResponseHandler

ここから、流れは以下のようになっていることがわかります。

  1. クライアントからリクエストを受け取る
  2. OperationMiddlewareの前処理を実施
  3. Operation実行 = dataerrorsextensionsといった、クライアントが見るレスポンス全体データを生成するResponseHandlerを作成
  4. OperationMiddlewareの後処理を実施
  5. ResponseMiddlewareの前処理を実施
  6. ResponseHandlerを実行
  7. RootFieldMiddlewareの前処理を実施
  8. ルートリゾルバを実行して、レスポンスのdataフィールドに入れるデータを取得する
    • FieldMiddlewareの前処理を実行
    • レスポンスに必要なデータを集めて、レスポンスフィールドを作る
    • FieldMiddlewareの後処理を実行
    • 必要なフィールドを全て作るまで1に戻って繰り返す
  9. RootFieldMiddlewareの後処理を実施
  10. ルートリゾルバが集めてきたデータをjsonエンコードして、レスポンスのdataフィールドに格納
  11. ResponseMiddlewareの後処理を実施

次章予告
次は、「ディレクティブを利用した認証機構の追加」をご紹介したいと思います。

今日は以上です。
ありがとうございました。
よろしくお願いいたします。

0
0
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
0
0