20211127のGoに関する記事は4件です。

【Go言語】はじめてのGraphQLサーバ実装 | gqlgen

この記事について この記事は、Go言語でGraphQLサーバの開発をする方法を書きます。 この記事で書くこと GraphQLのスキーマ定義 Go言語でGraphQLサーバの開発方法 フレームワークとしてgqlgenを利用 エラー処理 負荷対策 N+1問題 クエリの大きさ判定 この記事で書かないこと Go言語の説明 GraphQLの概要について バックエンドのデータベース(MySQL等)への接続方法 ソースコード この記事で紹介するコードはGithubに置いております。 https://github.com/hiroyky/go_graphql_server_sample 前提条件 Go言語の基本的な知識があり、お使いのマシンでGo言語のビルドや実行ができる。 GraphQLの概要について知識がある。 GraphQLの概要は公式が詳しく説明をしています。 また、2021年11月23日現在 Go-1.17で試験しております。 開発の開始 GraphQLのフレームワークgqlgenを使ってGraphQLサーバの構築を始めます。 初期化 新規リポジトリに描きコマンドを順に実行してGo言語プロジェクトの初期化、GraphQLのフレームワークgqlgenの導入を行います。 $ go mod init $ go get github.com/99designs/gqlgen $ go run github.com/99designs/gqlgen init これによって下記のファイル群が作成されています。 ├── gqlgen.yml: コード自動生成の設定ファイル ├── graph │   ├── generated │   │   └── generated.go // 編集しないファイル │   ├── model │   │   └── models_gen.go // GraphQLで使う型が定義されているファイル │   ├── resolver.go // リゾルバの定義 │   ├── schema.graphqls // GraphQLのスキーマ定義ファイル │   └── schema.resolvers.go // 実際の処理を書くファイル └── server.go // HTTPサーバの起動プログラム この段階でも下記を実行すれば、http://localhost:8080/ でプレイグラウンド画面を開くことができます。 $ go mod tidy $ go run ./server.go 下図のプレイグラウンド画面でGraphQLを試すことができます。 ここまでの内容をこちらに設置しました。 https://github.com/hiroyky/go_graphql_server_sample/pull/1 GraphQLサーバ開発の流れ 先ほどの自動生成で、下記の3ファイルも実装され内容は以下のようになっています。 schema.graphqlsにTODOリストを模したスキーマが定義されています。 model/model_gen.goにGo言語でその型定義が記述されています。 schema.resolvers.goに処理を実装するための雛形が記載されています。 プログラムの作成手順は以下の流れになります。 *.graphqlsにGraphQLのスキーマ定義を書く。 gqlgenの自動生成コマンド(gqlgen)を実行する。 作成・更新されたmodel/model_go.goファイルを確認し、必要に応じて別ファイルに移して編集する。 *.resolvers.goファイルに処理本体を実装する。 *.graphqlsファイルと*.resolvers.goファイルは対になっています。 GraphQLのスキーマ定義(の復習) GraphQLのスキーマ定義は、データの型定義と関数(メソッド)の定義で行います。どちらもGo言語やその他の言語の構造体・クラスの定義と同じ要領です。 型定義では、構造体名を決めて中身のフィールドを定義してきます。フィールドの型の基本形としてString, Int, Booleanが用意されています。他に、プライマリキーであることを示すID、列挙対、インターフェイス、独自型定義の仕組みが用意されています。 インターフェイスは共通するフィールドを定義します。インターフェイスを実装した型はかならずそのフィールドを持たなければイケません。プログラム言語にあるインターフェイスとほぼ等価と言えるでしょう。 関数(メソッド)の定義は2つに分かれています。取得系を定義するqueryと更新系を定義するmutationです。どちらも関数名、引数、戻り値を記述します。 公式の説明「Schemas and Types」 スキーマ定義とコード生成 ”会社と部署、会社員”という題材で設計 自動生成されたTODOリストのスキーマを題材に進めても良いのですが、ここでは会社(company)と部署(department),会社員(employee)というよくある題材で考えてみたいと思います。題材をMySQLのデータベーステーブルのER図で表すと下記のような構成です。本記事ではバックエンドのデータベースには触れませんがMySQLを使っている想定で話を進めます。 graph/model/schema.graphqlsとgraph/model/scheme.resolvers.goを削除して新規にスキーマ定義ファイルを作成しましょう。 $ rm ./graph/model/schema.graphqls $ rm ./graph/model/schema.resovers.go スキーマを定義する1[基本] 最初なので型定義の基本的な部分から順番に行いましょう。いずれもgraph/に設置します。 最初に全体で共用するインターフェイスNodeを定義します。プライマリキーidを持つ型にはNodeインターフェイスを実装するのがお作法のようです。また独自型としてTimestampを定義しています。型末尾の!は必須である(NULL値を許容しない)ことを示しています。 common.graphqls interface Node { id: ID! } scalar Timestamp それでは早速、インターフェイスNodeの実装としてCompany(会社), Department(部署),Employee(社員)の型を定義していきましょう。Employeeでは列挙対Genderも定義しています。(昨今では性別にMale, Female以外にも対応すべきと言う議論もありますが、ここでは無しで) company.graphqls type Company implements Node { id: ID! companyName: String! representative: String! phoneNumber: String! } department.graphqls type Department implements Node { id: ID! departmentName: String! email: String! } employee.graphqls type Employee implements Node { id: ID! name: String! gender: Gender! email: String! latestLoginAt: Timestamp! """ 扶養家族の人数 """ dependentsNum: Int! """ 管理職かどうか """ isManager: Boolean! } enum Gender { Male Female } Go言語や他の言語の構造体・クラスでのメンバ変数の定義とほぼ同じですね。 スキーマを定義する2 [他の型と紐付け] それでは、更に型同士の紐付けも定義に組み込んでいきましょう。GraphQLの型定義ではリレーショナルデータベースでのリレーション(関係性)を持たせることができます。 まずはEmployeeから編集します。departmentとcompanyフィールドを追記します。こうすることでEmployeeから所属しているDepartmentやCompanyをたどれるようになります。そういえばデータベースのER図にはdepartment_idやcompany_idがありましたね。 employee.graphqls type Employee implements Node { id: ID! name: String! gender: Gender! email: String! latestLoginAt: Timestamp! """ 扶養家族の人数 """ dependentsNum: Int! """ 管理職かどうか """ isManager: Boolean! department: Department! # 追記 company: Company! # 追記 } enum Gender { Male Female } スキーマを定義する3 [ページネーション] 続いてDepartmentやCompanyにも同様に関連テーブルへの紐付けを追加していこうと思います。CompanyからDepartment、Employeeは1対多の関係です。従って配列で値を返すことになります。しかしながらAPIとして全件を毎回一括で返すわけにもいかないので、ページネーションを定義しましょう。(全件を一括返すような設計では、データベースが負荷で落ちますからね。) また今回はLimit,Offsetによる一般的なページネーションを採用します。多くのリレーショナルデータベースではlimit,offsetで取得件数・取得位置を指定しますが、それと同じです。GraphQLではカーソル式のページネーションを採用する場合も多いようですが、今回は見送ります。 個人的な意見として、カーソル式のページネーションは、ツイッターやフェイスブックのタイムラインのように時々刻々とレコードが挿入されるデータには適していると思いますが、そうでないデータであれば、無理矢理カーソル式のページネーションを採用するのではなくLimit,Offsetによるページネーションの採用も検討すべきと思います。今回は会社と社員一覧と時々刻々と変化するデータでは無いのでLimit,Offsetによるページネーションが適していると考えます。 ページネーション定義の準備としてcommon.graphqlsに下記を追記します。 interface Pagination { pageInfo: PaginationInfo! nodes: [Node!]! # Node型の配列という意味 } type PaginationInfo { page: Int! paginationLength: Int! hasNextPage: Boolean! hasPreviousPage: Boolean! count: Int! totalCount: Int! } それではDepartmentに紐付けを追加します。EmployeePaginationでページ位置情報とEmployeeの配列をフィールド名nodesで定義しています。 employees.graphqls # 下記を追記 type EmployeePagination implements Pagination { pageInfo: PaginationInfo! nodes: [Employee!]! } department.graphqls type Department implements Node { id: ID! departmentName: String! email: String! company: Company! # 追記 employees: EmployeePagination! # 追記 } 同様にCompanyにも紐付けを追加します。 department.graphqls # 下記を追記 type DepartmentPagination implements Pagination { pageInfo: PaginationInfo! nodes: [Department!]! } company.graphls type Company implements Node { id: ID! companyName: String! representative: String! phoneNumber: String! departments: DepartmentPagination! # 追記 employees: EmployeePagination! # 追記 } # 後ほど使うので併せて定義 type CompanyPagination implements Pagination{ pageInfo: PaginationInfo! nodes: [Company!]! } スキーマを定義する4 [queryとmutation] ここまでは型定義でしたが、関数(メソッド)の定義を行います。取得系のqueryと更新系のmutationを定義します。これらをラップする型名はそれぞれQuery, Mutationである必要があります。 関数の定義方法は一般的なプログラミング言語のそれと似ているので直感的に分かると思います。!が引数に付いていれば必須項目となり、戻り値についていればレスポンスがNULLで無いことが保証されているという意味になります。 関数名(引数1: 引数の型1!, 引数2: 引数の型2..): 戻り値の型 queryでは各型に対して単数形と複数形で取得関数を定義しています。いずれもlimit,offsetで取得件数と取得位置を指定できるようにしています。なおlimitは必須になっています。employeesではそれ以外にも絞り込み項目を定義しました。 query.graphqls type Query { company(id: ID!): Company companies(limit: Int!, offset: Int): CompanyPagination! department(id: ID!): Department departments(limit: Int!, offset: Int): DepartmentPagination! employee(id: ID!): Employee employees( limit: Int!, offset: Int, email: String gender: Gender, isManager: Boolean, hasDependent: Boolean ): EmployeePagination! } 更新系のミュテーションをmutation.graphqlsに定義します。ミュテーションの引数はinputとしてまとめています。引数の型定義はtypeではなくinput Xxxx {}で記述します。戻り値は、作成・更新した場合には新しい値を、削除の場合はいったんtrueを固定で返す定義です。 mutation.graphqls type Mutation { createCompany(input: CreateCompanyInput!): Company! updateCompany(input: UpdateCompanyInput!): Company! deleteCompany(id: ID!): Boolean! createDepartment(input: CreateDepartmentInput!): Department! updateDepartment(input: UpdateDepartmentInput!): Department! deleteDepartment(id: ID!): Boolean! createEmployee(input: CreateEmployeeInput!): Employee! updateEmployee(input: UpdateEmployeeInput!): Employee! deleteEmployee(id: ID!): Boolean! } input CreateCompanyInput { companyName: String! representative: String! phoneNumber: String! } input UpdateCompanyInput { id: ID! companyName: String representative: String phoneNumber: String } input CreateDepartmentInput { departmentName: String! email: String! } input UpdateDepartmentInput { id: ID! departmentName: String email: String } input CreateEmployeeInput { name: String! gender: Gender! email: String! dependentsNum: Int! isManager: Boolean! } input UpdateEmployeeInput { id: ID! name: String gender: Gender email: String dependentsNum: Int isManager: Boolean } ここまでの更新をGithubのプルリクにまとめました。ブランチはfeat2です。 https://github.com/hiroyky/go_graphql_server_sample/pull/2/files コード生成 コード生成1 [gqlgenを実行] スキーマ定義ができたらgqlgenコマンドでスキーマ定義を元にGo言語のプログラムを自動生成します。 $ go run github.com/99designs/gqlgen 下記のような構成でGo言語のファイルが生成されていると思います。 ./graph ├── generated │   └── generated.go ├── model │   └── models_gen.go ├── mutations.resolvers.go ├── query.resolvers.go └── resolver.go query.resolvers.goとmutations.resolvers.goには、先ほど定義した関数がGo言語の関数で定義されています。 引数の定義で!を付けて必須化した引数は値ですが、必須で無い引数はポインタ型になっています。未指定の場合はNULLが入ります。 query.resolvers.go // ・・・・ func (r *queryResolver) Employee(ctx context.Context, id string) (*model.Employee, error) { panic(fmt.Errorf("not implemented")) } func (r *queryResolver) Employees(ctx context.Context, limit int, offset *int, email *string, gender *model.Gender, isManager *bool, hasDependent *bool) (*model.EmployeePagination, error) { panic(fmt.Errorf("not implemented")) } // ・・・・ コード生成2 [構造体を編集して再生成] ところで、model/models_gen.goで定義した型を確認しましょう。Employeeは次のように定義されています。スキーマ定義に書いたコメントも反映されていますね。 model/models_gen.go type Employee struct { ID string `json:"id"` Name string `json:"name"` Gender Gender `json:"gender"` Email string `json:"email"` LatestLoginAt string `json:"latestLoginAt"` // 扶養家族の人数 DependentsNum int `json:"dependentsNum"` // 管理職かどうか IsManager bool `json:"isManager"` Department *Department `json:"department"` Company *Company `json:"company"` } さて、ここで着目しなければいけないのはDepartmentとCompanyです。一見問題ないように見えますが、問題があるのです。 バックエンドのデータベースにMySQL等のリレーショナルデータベースを使ってる場合、データベースのテーブルとしてはDepartmentとCompanyが直接入るのではなくdepartment_id,company_idといった外部キーのIDになると思います。そしてCompanyやDepartmentの中身が欲しいときは、別途SQLを実行するか結合を含んだSQLを実行する必要があります。GraphQLではリクエストのクエリにDepartmentやCompanyの指定があったときだけ別途SQLを実行するような柔軟な仕様にできます。つまづきポイントの1つだと思うのでまずは一緒にやってみましょう。 自動生成のモデル型を変更します。ただmodels/models_gen.goは自動生成の度に再生成されるのでこのファイルを直接編集はしません。代わりに同じディレクトリ(パッケージ)にmodels/models.goを新規作成して下記のように記述します。その際にmodels_gen.goのEmployee構造体を削除します。 着目して欲しいのは Department *DepartmentやCompany *Companyを削除して代わりにDepartmentID stringとCompanyID stringを定義しています。これでデータベースのテーブル定義に近づきました。IDはint型では?という声が聞こえてきそうですがstring型にします。理由は後述します。 model/models.go package model type Employee struct { ID string `json:"id"` Name string `json:"name"` Gender Gender `json:"gender"` Email string `json:"email"` LatestLoginAt string `json:"latestLoginAt"` // 扶養家族の人数 DependentsNum int `json:"dependentsNum"` // 管理職かどうか IsManager bool `json:"isManager"` DepartmentID string `json:"department"` // Departmentを削除して、代わりにDepartmentIDを記述 CompanyID string `json:"company"` // Companyを削除して、代わりにCompanyIDを記述 } func (Employee) IsNode() {} ここまで編集したら改めてgqlgenコマンドを実行します。 $ go run github.com/99designs/gqlgen するとどうなるでしょうか?新たにemployee.resolvers.goが生成されました。これには2つの関数が定義されています。第二引数でEmployeeを受け取り、DepartmentやCompanyを返す関数です。 これは、先ほど自身で定義し直したEmployeeについてgqlgenがDepartmentとCompanyが不足していたことを検知して、不足分を取得するための関数を定義したのです。関数の第2引数では親であるEmployeeが渡されるのでそれを手がかりに子のCompanyやDepartmentを取得する処理を実装します。 employees.resolvers.go func (r *employeeResolver) Department(ctx context.Context, obj *model.Employee) (*model.Department, error) { panic(fmt.Errorf("not implemented")) } func (r *employeeResolver) Company(ctx context.Context, obj *model.Employee) (*model.Company, error) { panic(fmt.Errorf("not implemented")) } これらの関数はクライアントがEmployeeのクエリでdepartmentやcompanyを指定したときのみ実行されます。必要でなければ呼び出されません。たとえば以下のリクエストクエリではdepartment()は呼び出されますがcompany()は指定していないので呼び出されません。 query { employee(id:"RW1wbG95ZWU6MQ==") { id name department { id departmentName } } } さて、それでは他のCompanyやDepertmentの構造体も同様に編集していきます。models_gen.goからmodels.goに移動して追加削除をします。 model/models.go type Company struct { ID string `json:"id"` CompanyName string `json:"companyName"` Representative string `json:"representative"` PhoneNumber string `json:"phoneNumber"` // Departmentsのフィールド自体を削除 // Employeesのフィールド自体を削除 } func (Company) IsNode() {} type Department struct { ID string `json:"id"` DepartmentName string `json:"departmentName"` Email string `json:"email"` CompanyID string `json:"company"` // Companiesを削除して、CompanyIDを追記 // Employeesのフィールド自体を削除 } func (Department) IsNode() {} ここまで編集したら改めてgqlgenコマンドを実行します。 $ go run github.com/99designs/gqlgen 先ほどと同様に構造体とGraphQLのスキーマ定義で乖離している部分をgqlgenが検知して不足部分を補うための関数が生成されました。例えば新規作成されたcompanies.resolvers.goは以下のように親であるCompanyを第二引数に受けてDepartmentPagination, EmployeePaginationを返す関数が定義されています。 companies.resolvers.go func (r *companyResolver) Departments(ctx context.Context, obj *model.Company) (*model.DepartmentPagination, error) { panic(fmt.Errorf("not implemented")) } func (r *companyResolver) Employees(ctx context.Context, obj *model.Company) (*model.EmployeePagination, error) { panic(fmt.Errorf("not implemented")) } ここまでの編集をプルリクにまとめました。ブランチはfeat3です。 https://github.com/hiroyky/go_graphql_server_sample/pull/3 中身の実装とプライマリキーの注意点 さて、ここからは自動生成されたリゾルバ関数の中身の実装を行います。バックエンドのデータベースに接続したり、別のREST APIにリクエストしたりなどです。本記事ではデータベースやREST APIとの通信は趣旨からずれるので記述しません。ぜひご自身で中身の実装をすすめてください。 ただ、1点注意事項があります。それはプライマリキーIDを型を横断してグローバルでユニークにするということです。つまり、Employeeのidが1,2,3...と連番でDepartmentのidも1,2,3..と連番ではIDはグローバルでユニークではありません。そこでプライマリキーを次のように型名とセットで記述します。 Employee:1 Employee:2 Employee:3 ... Department:1 Department:2 Department:3 ... GraphQLでは更にこれをBase64エンコードした物をプライマリキーとして扱うのが一般的です。 base64(Employee:1) = "RW1wbG95ZWU6MQ==" base64(Department:1) = "RGVwYXJ0bWVudDox これを実現するために構造体のidをint型ではなくstring型にしました。バックエンドデータベースのMySQL等ではintの連番でプライマリキーを管理していても、GraphQLのレスポンスやリクエスト引数では型名とセットにした文字列(型名:番号)のbase64文字列に変換すべきです。理由は、GraphQLでは、型関係なくID自体でキャッシュ等を管理するので、IDの重複を避けるためです。従って、プライマリキーにUUIDなどのそれ自体が唯一無二な形式を用いているのであれば変換をする必要はありません。 Global Object Identificationに解説があります。 エラー処理 リゾルバでエラーを生成して、エラーレスポンスを返す必要な場面があります。gqlgenフレームワークはその機能も備えています。 GraphQLのレスポンスJSONは正常系であればでdataのみを返しますが、エラーレスポンスではerrors配列を返します。エラーには次の項目を含めます。 message: エラーの内容を簡潔に伝えるメッセージ path: エラーが起こった場所(クエリの位置) extensions: それ以外にクライアントに伝えたい内容があればkey:value形式で記述 { "data": {}, "errors": [ { "message": "Error: hoge fuga", "path": [ "employee" ], "extensions": {"key1": "value1"} }, { "message": "Error: foo bar", "path": [ "department" ] }, ] } これを実現するために server.goのgraphqlサーバを編集します。サーバインスタンスのSetErrorPresenterの引数にエラーレスポンスを生成する関数を書きます。引数のerrorにリゾルバ等で投げられたエラーが入るのでそれを元にエラーレスポンスを生成します。gqlerror.Error型をこの関数で返します。 server.go srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) // エラー処理を書く srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error { err := graphql.DefaultErrorPresenter(ctx, e) err.Message = e.Error() err.Extensions = map[string]interface{}{ "key1": "value1", "key2": "value2", } return err }) この内容をプルリクにしています。ブランチはfeat4 https://github.com/hiroyky/go_graphql_server_sample/pull/4 公式の文献は https://gqlgen.com/reference/errors/ 負荷対策 N+1問題対策 概要 GraphQLあるあるの問題としてN+1問題があります。クエリで子要素を参照したときに、都度リゾルバが実行されるため、バックエンドのデータベースやAPIに大量のリクエストが発生する現象です。 例えば、下記のようにEmployeeの配列から所属Companyを参照するクエリを書いた場合です。この時、company()リゾルバが都度実行されます(最大100回)。従ってバックエンドのデータベース等へもリクエストが都度発生するためデータベースへの負荷が大きくなります。 query { employees(limit:100) { nodes { name company { companyName } } } } SELECT * FROM companies WHERE company_id=1; SELECT * FROM companies WHERE company_id=1; SELECT * FROM companies WHERE company_id=2; SELECT * FROM companies WHERE company_id=3; -- ・・・ この「N+1問題」の解決策としてdataloaderを用いた手段があります。これは毎回バックエンドへリクエストを発生するのではなく、ある一定期間処理を溜めてからバックエンドにリクエストをまとめて送るようにするものです。これにより下記のように1クエリにまとめられます。 SELECT * FROM companies WHERE company_id IN (1,2,3); 導入・実装 実現するために dataloadenを利用します。 $ go get github.com/vektah/dataloaden 他の言語のdataloaderではジェネリクス型等で対応するようですが、Go言語のdataloaderはコード生成します。 $ mkdir dataloader $ cd dataloader $ echo "package dataloader" > gen.go $ go run github.com/vektah/dataloaden CompanyLoader string "*github.com/hiroyky/go_graphql_server_sample/graph/model.Company" dataloader/companyloader_gen.goが生成されました。Companyの取得では、このCompanyLoaderを使うように編集してみましょう。 まずは各リゾルバから参照できるようにresolver.goにフィールドを設けます。 graph/resolver.go type Resolver struct{ CompanyLoader *dataloader.CompanyLoader // 追記 } server.goでGraphQLサーバ起動時にCompanyLoaderを生成して、渡すようにします。(main関数に書くべき内容ではないと思いますが、簡略化のためにmain関数に書きます。) Fetchにデータをまとめて取得する処理を書きます。引数のkeysには、呼び出し時に溜めた引数がまとめて渡されます。keysの順番通りに値を返します。 server.go func main() { // ・・・省略・・・ companyLoader := dataloader.NewCompanyLoader(dataloader.CompanyLoaderConfig{ MaxBatch: 100, // 溜める最大数、0を指定すると制限無し Wait: 2 * time.Millisecond, // 溜める時間 Fetch: func(keys []string) ([]*model.Company, []error) { companies := make([]*model.Company, len(keys)) errors := make([]error, len(keys)) // 取得処理を書く SELECT * FROM company WHERE company_id IN (...) // 引数のkeysに対応する順番の配列で返す。 return companies, errors }, }) srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{ CompanyLoader: companyLoader, }})) // ・・・省略・・・ } 呼び出しは以下のように行います。Company.Loadを呼び出すことで、先ほどFetchに定義した関数が実行されます。即時都度実行されるわけではなく、溜めてからまとめて実行されます。 employee.resolvers.go func (r *employeeResolver) Company(ctx context.Context, obj *model.Employee) (*model.Company, error) { return r.CompanyLoader.Load(obj.CompanyID) } dataloaderの生成コマンドについて改めてまとめます。 $ cd dataloader $ echo "package dataloader" > gen.go ■int型を引数に取る関数でロードしたい場合 先ほどはコマンドの第二引数に`string`を指定しました。これは`Fetch関数`の引数の型に対応します。従って`CompanyLoader.Load()`関数の引数を`int`型にしたい場合は`int`を指定します。 $ go run github.com/vektah/dataloaden CompanyLoader int "*github.com/hiroyky/go_graphql_server_sample/graph/model.Company" ■配列を戻り値のロード関数を生成したい場合 `Load()`関数で配列を戻り値にすることもできます。`Fetch`関数の配列処理の実装が少し複雑になるので注意してください。おそらく二重のforループが発生するため。 $ go run github.com/vektah/dataloaden CompaniesLoader int "[]*github.com/hiroyky/go_graphql_server_sample/graph/model.Company" 以上、N+1問題を解決するためにdatalodaerを導入しました。 ここまでをプルリクにまとめました。ブランチはfeat5 https://github.com/hiroyky/go_graphql_server_sample/pull/5 公式文献: - https://gqlgen.com/reference/dataloaders/ - https://github.com/vektah/dataloaden クエリの重さ制限 概要 GraphQLのAPIでは、クライアント側が自由にクエリを作成してリクエストすることができます。一方で、負荷が重いクエリも簡単にリクエストできてしまう問題があります。そこでクエリを実行する前に、クエリの重さを計算して一定以上であれば実行せずにエラーレスポンスを返すという機能があります。 GraphQLのAPI自体をグローバルに公開する場合には導入が必須かとは思いますが、アクセス元が社内サーバに限定されるなどの場合には、アクセス元のシステムを開発しているチームとクエリ内容を事前に相談するという解決策でも良いかと思います。 導入・実装 導入の仕方自体は簡単で、端的に言えば下記一行をserver.goに足すだけです。引数に重さの上限を指定します。1項目辺り1として計算され、この値を超えたらエラーレスポンスを返します。 srv.Use(extension.FixedComplexityLimit(10)) // 重さが10を超えたらエラーにする しかしながら、全ての項目が等しい負荷であるわけがありません。例えば配列を返す項目や、更新系のミュテーションは通常よりも重いクエリのはずです。そこで、スキーマの項目毎に計算関数を定義できます。 これまではサーバのNew関数に直接渡していた引数である設定を編集します。 server.go srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{CompanyLoader: companyLoader}})) 上記を下記のようにします。c.Complexity以下で重さを計算する関数を定義します。関数の第一引数にはリクエストで要求されたレスポンスの項目数、第二引数にはリクエストの引数が与えられます。それらを元に重さを計算して数値で返します。 server.go c := generated.Config{Resolvers: &graph.Resolver{CompanyLoader: companyLoader}} c.Complexity.Mutation.CreateCompany = func(childComplexity int, input model.CreateCompanyInput) int { return 5 } srv := handler.NewDefaultServer(generated.NewExecutableSchema(c)) 以上、クエリの重さ計算の処理でした。 といいつつも、アクセス元が社内サーバだけに限定してアクセス元の開発チームとクエリ内容を事前相談しておくということが可能であれば、そうしたほうが良い気もしますが.. まとめ gqlgenを使ったGO言語でのGraphQLサーバの実装について記述しました。 gqlgenでの開発手順は下記の手順でした。これの繰り返しです。 スキーマ定義 コード生成 生成されたコードの調整 コード生成 リゾルバに処理を書く 他に、エラー処理や負荷対策を記述しました。 拝読ありがとうございました。お疲れ様でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang/ goenv】goenv install -lで古いバージョンしか表示されないときの解消法

原因 goenvのバージョンが古い 解消方法 アンインストールしてから新しいバージョンをインストール brew uninstall goenv && brew install --HEAD goenv で解消しました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSLambda(Golang)×DynamoDBのローカル開発環境を整える

目的 AWS LambdaでバックエンドAPIを構築しているとき、関数の挙動を確認したいときは都度devにデプロイしていました。流石にローカルでの開発環境を整えたいと思ったのですがGoでの使い方の情報が少なかったので個人メモとしてまとめてみました。 今回はAPI GatewayがトリガーのLambda関数になります。 APIトリガー以外は →serverless invoke local -f <function> で十分らしいです 全体の流れ 以下のJavascript filesのところを今回はGolangにしてやってみます。 引用:https://qiita.com/noralife/items/e36621ddd0e5b8ff4447#%E6%A7%8B%E6%88%90 大まかな流れは以下です。 1. DynamoDB Localをインストール&セットアップ 2. Serverless offlineをインストール &セットアップ DynamoDB Localをインストール&セットアップ パッケージインストール $ npm install --save-dev serverless-dynamodb-local serverless.ymlにプラグインを追記 serverless.yml plugins: - serverless-dynamodb-local JDKがなければここでインストール dynamodb-localのためにはJavaが必要でしたので、ローカルpcにJavaを入れていない方はインストールします。 インストールの仕方は、mac osだと、以下が参考になります。 https://qiita.com/suke_masa/items/f9af0fb84ad9447ae961 dynamo db localをインストール serverless.ymlのあるディレクトリにて、 $ sls dynamodb install カスタム定義 serverless.yml custom: dynamodb: stages: - dev start: port: 8000 inMemory: true migrate: true seed: true seed: development: sources: - table: テーブル名 この状態で、 sls dynamodb start でdynamodbがローカルで立ち上がります。 http://localhost:8000/shell にアクセスすると管理画面に移れる。 シェル例 var params = { TableName: 'pr_enqueteform_dev', }; dynamodb.scan(params, function(err, data) { if (err) ppJson(err); else ppJson(data); }); Serverless offlineをインストール &セットアップ 次にローカルでapi gatewayを試すためにServerless offlineをインストールしていきます。 パッケージインストール $ npm install --save-dev serverless-offline serverless.ymlにプラグインを追記 serverless.yml plugins: - serverless-dynamodb-local - serverless-offline ### 追記 カスタム定義 serverless.yml custom: serverless-offline: useDocker: true この状態で npx sls offline でローカルで起動できます。 ※Makefileにて、 make local-test を打つことでビルド〜offline起動までやってくれるようにしています。 例: $ npx sls offline Serverless: Deprecation warning: CLI options definitions were upgraded with <中略> ┌───────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ POST | http://localhost:3000/dev/submit │ │ POST | http://localhost:3000/2015-03-31/functions/EnqueteformReceiver/invocations │ │ │ └───────────────────────────────────────────────────────────────────────────────────────┘ offline: [HTTP] server ready: http://localhost:3000 ? offline: offline: Enter "rp" to replay the last request offline: POST /dev/submit (λ: EnqueteformReceiver) 上記みたいな感じになっていたら立ち上がっています? offlineとdynamodb-localを疎通させる localでofflineを使ってlambda apiを叩いてログを出力してみると、 handlerのeventsの中の、 events.APIGatewayProxyRequest.RequestContext.APIID が "offlineContext_apiId" となっていました。 そこで、この部分で判断して、localのdynamodbクライアントを生成するか本番用のdynamodbクライアントを生成するかを条件分岐させることにします。 localのdynamodbクライアントの生成方法は以下でやります。 func (c *DynamoDBClient) OfflineDynamoDBClient(region string, tableName string) *DynamoDBClient { sess := session.Must(session.NewSession()) config := aws.Config{ Region: aws.String(region), Endpoint: aws.String("http://host.docker.internal:8000"), DisableSSL: aws.Bool(true), Credentials: credentials.NewStaticCredentials("dummy", "dummy", "dummy"), } dynamodbClient := DynamoDBClient{ tableName: tableName, dynamo: dynamodb.New(sess, &config), } return &dynamodbClient } ※もっとよい判定方法がありそうな感じです。 試してみる ローカルでコードを編集した後、一度ビルドしなければローカルの方にも反映されませんので、 make clean make build npx sls offline とします。 curlで疎通してみる sls offlineしたターミナルとは別のタブにて、 curl http://localhost:3000/dev/submit -X POST -H "Content-Type: application/json" -d '{"category":"hogecategory","email":"sample@gmail.com","address":"東京都", "hoge":{"id":"1001","text":"hogehoge"}}' sls offlineしたターミナルのタブにてログが出てくる。 START RequestId: d09*7296d-***-1c72-545b-5**c58d99034 Version: $LATEST <中略> END RequestId: d09729?6d-b4e2-1c72-5*45b-5e**ec58d99034 REPORT RequestId: d09?7296d-*b4e2-1c72-545b-5c5*d99034 Init Duration: 189.68 ms Duration: 5513.25 ms Billed Duration: 5514 ms Memory Size: 3008 MB Max Memory Used: 34 MB 実際にローカルのdynamoDBにデータを挿入する関数であれば、 http://localhost:8000/shell をみてみると挿入されていることが確認できます! 参考 Serverless Framework での AWS Lambda + Go ローカル開発事情 Serverless アプリケーションをローカルで開発する serverless framework + TypeScript +DynamoDB のローカル環境 Go で DynamoDB Local を使った時にいろいろハマったのでメモ example
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで Quick Select を書いた

日本語の資料が乏しかったので,ここにまとめます。 必要な前提知識も多く、この記事単体では完結していないです。 詳しくは、参考文献等をご参照ください。 また、書かれているコードは、以下の問題で検証済みですが、 不十分であるため、コピペされる方は自己責任でお願いいたします。 Quick Select とは 「ある配列の中で、k番目に小さい値はいくつ?」に答えるアルゴリズム。 クイックソートの要領で pivot を選択し、pivotを元に、配列を分割しながら答えを探す。 愚直に考えると、ソートしてk番目をとればいいが、その場合は、最悪計算量がソートがボトルネックになり$O(nlogn)$になる。 一方、このアルゴリズムは、平均計算量$O(n)$で解くことができる。 実装方針 pivot の選択方法には、以下の方法があるが、今回は中央値の中央値をpivotにすることにした。 常に左の要素を採用する ランダムに取得した要素を採用する 中央値の中央値を採用する また、pivotに対して、配列を並べ替える戦略が以下にあるが、効率がよい前者を採用した。 Hoare partition scheme Lomuto partition scheme コード func insertionSort(a []int, left, right int) { for i := left; i <= right; i++ { j := i for (j > left) && (a[j-1] > a[j]) { a[j-1], a[j] = a[j], a[j-1] j-- } } } func median(a []int, left, right int) int { insertionSort(a, left, right) return a[left+(right-left)/2] } func partition(a []int, left, right, pivot int) int { i := left - 1 j := right + 1 for { i, j = i+1, j-1 for a[i] < pivot { i += 1 } for a[j] > pivot { j -= 1 } if i < j { a[i], a[j] = a[j], a[i] } else { return j } } } func selectPivot(a []int, left, right int) int { if right-left < 5 { return median(a, left, right) } for i := left; i+4 <= right; i += 5 { insertionSort(a, i, i+4) a[i+2], a[left+(i-left)/5] = a[left+(i-left)/5], a[i+2] } n := right - left + 1 return innerSelect(a, left, left+n/5-1, left+n/10-1) } func innerSelect(a []int, left, right, kth int) int { if left == right { return a[left] } pivot := selectPivot(a, left, right) pivotIndex := partition(a, left, right, pivot) if kth <= pivotIndex { return innerSelect(a, left, pivotIndex, kth) } else { return innerSelect(a, pivotIndex+1, right, kth) } } func QuickSelect(a []int, left, right, kth int) int { return innerSelect(a, left, right, kth) } 参考文献 クイックセレクトの考え方の元となるクイックソートの詳細 https://en.wikipedia.org/wiki/Quicksort クイックセレクトの実装の概要 https://en.wikipedia.org/wiki/Quickselect http://www.dais.is.tohoku.ac.jp/~shioura/teaching/ad09/ad09-05.pdf https://en.wikipedia.org/wiki/Median_of_medians 中央値の中央値のわかりやすい解説 http://www.flint.jp/blog/?entry=109 実装がハマったときにみたサイト https://stackoverflow.com/questions/58331986/quickselect-with-hoare-partition-scheme
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む