GraphQLのいろいろな読み物を読んだ時のメモ
Introduction to GraphQL
- graphqlの仕様をまずはちゃんと読みましょう
- ざっと流し読んで知らないところだけメモる感じで
- fragment辺りの理解が怪しい
Query
{ human(id: "1000") { name height(unit: FOOT) } }
- このクエリ書けるのもしかして俺知らなかった?
- typeのfieldになんか渡すやつ出来るのか
- ルートのtypeは知ってたけど
{ leftComparison: hero(episode: EMPIRE) { ...comparisonFields } rightComparison: hero(episode: JEDI) { ...comparisonFields } } fragment comparisonFields on Character { name appearsIn friends { name } }
- aliasもここに出てくるのか
- ほんでaliasすると同じ項目をまとめたいからfragmentが便利ですと。よく出来たドキュメントだ。
- operationNameの有用性について
but its use is encouraged because it is very helpful for debugging and server-side logging. When something goes wrong (you see errors either in your network logs, or in the logs of your GraphQL server) it is easier to identify a query in your codebase by name instead of trying to decipher the contents.
Variables
- variableは動的なクエリストリングの書き換えよりも楽ちんなのでメリットが有るよねという話。言われてみればそうか...
Directives
- directiveも動的なクエリのための機構で、fragmentやfieldに対して付与することが出来る。
Mutation
Inline Fragment
query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name ... on Droid { primaryFunction } ... on Human { height } } }
Schemas and Types - GraphQL
公式ドキュメントの次の章。
- graphql-rubyでのinterfaceの書き方 https://graphql-ruby.org/type_definitions/interfaces
- 同じようなことをしたいけどそれぞれ違う...要はクラスの継承とだいたい同じでいいと思うんだけど微妙に使い所がわからない
GraphQL Best Practices - GraphQL
公式ガイド、これより前のセクションはだいたい知ってたのでこっち読んだ方が有用そうだったので読んでいく
- 認証とかネットワークとかは手に負えないからGraphqlのドキュメントに載ってないのではなくて、単純にGraphQLそのものというよりはプラクティスの類になるから、だそう
- あくまでプラクティスなので、ケースによっては意図的に無視したりしてもOK
- HTTP
- RESTとは違ってエンドポイントは1つ。複数持ってもいいけどしんどいのでおすすめしません
- JSON
- Versioning
- GraphQLのバージョニングは止められてないけどいろんなツールがあるから避けることをおすすめするよ
- 破壊的な変更が起こるとバージョニングが必要になる。GraphQLは要求されたものしか返さないので破壊的な変更を起こさずに新しい型を追加していくだけで良い。やったね!
- いやそう言っても色々あるでしょうに....
- Nullability
- 多くのシステムでnullは明示的に宣言する必要があるけど、GraphQLはデフォルトがnullだよ
- なぜならGraphQLの背後にはDBや非同期処理などが存在するため、外部要因でデータが不完全になるケースが多く存在するから、という思想に基づいているよ
- GraphQLのサーバーを作る時はあらゆる問題が起こり得ることを想定すると同時に失敗したフィールドに対してはnullが適切であることを覚えておくといいよ
- Pagenation
- Connectionとかね
- Server-side Batching & Caching
- common problemなのでDataLoaderとかで解決できるよ
Thinking in Graphs
考え方みたいなのいっぱい載ってる章。
Working with Legacy Data
- DBのスキーマに負債が溜まってたりような状況の時はそのままGraphQLのスキーマに反映させずにクライアント側に使ってほしい形で書くのが良い。
Build your GraphQL schema to express "how" rather than "what". Then you can improve your implementation details without breaking the interface with older clients.
- ここだけ言ってることが分からん
Serving over HTTP
HTTPとGraphQLの付き合い方について
Web Request Pipeline
殆どのウェブアプリのフレームワークはpipeline model(リクエストはたくさんのmiddlewareを通り抜けて処理されることを期待するモデルのこと)だけど、GraphQLは全ての認証middlewareの後ろに存在しているべき。そうすることで他のHTTPエンドポイントとセッションを共有できる。
Authorization
- 認可はビジネスロジックのレイヤに押し込めるべし。
var postType = new GraphQLObjectType({ name: ‘Post’, fields: { body: { type: GraphQLString, resolve: (post, args, context, { rootValue }) => { // return the post body only if the user is the post's author if (context.user && (context.user.id === post.authorId)) { return post.body; } return null; } } } });
- こういう型を作ると型の数だけ認可ロジックを作らないといけないので当然漏れる。
- (やってるなぁ........)
- ↓のが望ましい。
//Authorization logic lives inside postRepository var postRepository = require('postRepository'); var postType = new GraphQLObjectType({ name: ‘Post’, fields: { body: { type: GraphQLString, resolve: (post, args, context, { rootValue }) => { return postRepository.getBody(context.user, post); } } } });
- 改善したいンゴ〜
Pagination
- カーソルベースのページネーションをする時、フォーマットに依存しないようにbase64するのがいいよ
- カーソルを得るためにConnectionとnodeの間にedgeという層を挟むよ
Connection has many edges, edge has one node and cursor
になる- このedgeがnodeに関するメタ情報を持ってくれるのでGoodだよ
- さらに、あとどれぐらいページネーションできるかなどのConnection自体の情報を持つためにConnectinoの子にpageInfoを持つよ
- pageInfoはendCursor(レスポンスとして返したリストの最後のカーソル)やhasNextPageなどを返すよ
- おっ公式でfriendsConnectionって名前つけてるな
Global Object Identification
- nodeとIDの話
GraphQL - Authorization - graphql-ruby
- typeやargumentは
authorized?
のメソッドをデフォで持ってるのでこれを活用していく。- そもそもここで間違ってるんだよな...
- だいたいこんな感じで認可する。BaseObjectの場合はresolveの後に呼ばれて、falseを返すと結果を返さなくなる。
class Types::Friendship < Types::BaseObject # You can only see the details on a `Friendship` # if you're one of the people involved in it. def self.authorized?(object, context) super && (object.to_friend == context[:viewer] || object.from_friend == context[:viewer]) end end
- Fieldだとこんな感じ。↓ Fieldの
authorized?
はresolveの前に呼ばれる。
class Types::BaseField < GraphQL::Field # Pass `field ..., require_admin: true` to reject non-admin users from a given field def initialize(*args, **kwargs, require_admin: false, &block) @require_admin = require_admin super(*args, **kwargs, &block) end def authorized?(obj, args, ctx) # if `require_admin:` was given, then require the current user to be an admin super && (@require_admin ? ctx[:viewer]&.admin? : true) end end
- こういうのをquery_typeに書いてたりするので暇があったらドンドコ移していったほうが良いな
- profile周りどうすればいいかな.........
def wish_lists(user_id:) return [] unless request_from_owner?(user_id: user_id.to_i) || User.find(user_id)&.profile&.info_public? UserWishList.where(user_id: user_id) end private def request_from_owner?(user_id:) context[:current_user]&.id == user_id.to_i end
GraphQL - Directives - graphql-ruby
directiveで認可の設計できるんだっけ?ということを知りたい
- graphqlのdirectiveには2種類ある。
runtime directives
クライアント側から投げつける
@include
とか@skip
とかのdirective。クライアント側でクエリの形を動的に変えたい時に使うやつ。 schema directives スキーマに対してアノテートするディレクティブ。こういうの↓
type User { firstName @deprecated(reason: "Use `name` instead") lastName @deprecated(reason: "Use `name` instead") name }
GraphQL Advent Calendar 2020 - Qiita
なんか見っけた。
GraphQL - Lookahead - graphql-ruby
GraphQL-Rubyのv1.9からlookahead
という、クエリが要求したフィールドをType側で先読みできる機能ができたらしい。
field :files, [Types::File], null: false, extras: [:lookahead] def files(lookahead:) if lookahead.selects?(:full_path) # This is a query like `files { fullPath ... }` else # This query doesn't have `fullPath` end end
- よっぽど重い処理やってないと使わないと思うけど...
初めて Apollo Client を使うことになったらキャッシュについて知るべきこと - WASD TECH BLOG
取得するフィールドに id は必ず含める 更新処理のときは Mutation のレスポンスでオブジェクトのキャッシュを更新する 作成、削除処理のときは refetchQueries などを使い配列のキャッシュを更新する 画面表示のたびに最新のデータを表示したければ fetchPolicy: "cache-and-network" を使う
やっぱapolloとかシュッとしたGraphQL client使ってるとこの辺がスタンダードなんだろうな
Shopify/graphql-design-tutorial - GitHub.com
takadaくんがtimesに貼ってたやつ。
- 中間テーブルがDBに存在しても、ビジネスのドメインで意味がないんだったらスキーマには含めないほうが良い。
特定可能な主要なビジネスオブジェクト(例えば商品やコレクションなど)はNodeインターフェースを実装するべきです。
type Collection implements Node { id: ID! }
Protip: リストのように、Booleanもほとんどいつも非Nullです。 NullableなBooleanを使う場合は、本当に3状態(Null/false/true)を区別する必要があるのかという点と、設計上のより大きな問題を招かないことを確認ましょう。* 手動コレクションの場合にこのフィールドは一体どの値をもつべきでしょうか。 trueとfalseのいずれもミスリーディングのように感じます。 かといってNullableにしたところで3状態を表現するフラグは、自動コレクションの場合に違和感を覚えます。 ルール #6: 密接に関連する複数のフィールドはサブオブジェクトにまとめること。
bodyHtml
は詳細設計なのでdescriptionに変えた上でHtml typeを作るのが良いって言ってるなー- たしかにこのケースだとコンテキスト表すのに型作ったほうが良いか
2つ目は、こちらのほうが大きな問題です、クライアントに実装を要求するという点です。 これは設計哲学における最も重要なポイントのひとつで、つねにサーバーこそが唯一の正しいビジネスロジックの情報源であるべきです。 ほとんどの場合でAPIは複数のクライアントに対して提供されます。 もしそれぞれのクライアントが同じロジックを実装しなければならないとしたら、それはコードの重複を生み、不必要なタスクとエラーが発生させる余地を伴います。 ルール #12: APIはデータだけではなくビジネスロジックを提供すること。複雑な計算はサーバーで為されるべきで、複数のクライアントではない。
- たしかにこのケースだとコンテキスト表すのに型作ったほうが良いか
Mutationについて
- 肥大化するupdateにどう立ち向かうのか?
別々のmutation(addProductやremoveProductなど)に分ける方法。柔軟、効率的で、最もうまくいく。
- 急にポジション取っててウケる
ルール #15: 関連に対する操作は複雑で、ひとつの便利な指針で語ることはできない。 ルール #19: フォーマットが明確であり、クライアント側の検証が複雑な場合には、入力に対して弱い型付け(EmailではなくString)を行うこと。これによりサーバーは一度にすべての検証を実行し、単一のフォーマットでエラーを報告することになり、結果としてクライアントが非常にシンプルになる。
- 分かりみがある
- updateとcreateのIFの話
- 共通にしちゃった
type Mutation { # ... collectionCreate(collection: CollectionInput!) collectionUpdate(collectionId: ID!, collection: CollectionInput!) } input CollectionInput { title: String ruleSet: CollectionRuleSetInput image: ImageInput description: String }
ルール #21: たとえいくつかのフィールドの必須制約を緩和する必要があっても、重複を減らすためにmutationの入力を構造化すること。
- これは意外だなぁ...
各mutationはその他の必要な情報に加えてユーザエラーフィールドを含む"payload"型を定義すべきです。 createのmutationはおそらく以下のようになるでしょう。
type CollectionCreatePayload { userErrors: [UserError!]! collection: Collection } type UserError { message: String! # Path to input field which caused the error. field: [String!] }
data: {"CollectionCreatePayload": {"collection": {...}, userErrors: [...]}
を許容するということかー
Designing GraphQL Mutations - apollo blog
後方互換性を保ちながらmutationを設計するためのtips。
Naming
#{動詞}#{名詞}
で構成するのが良い- Shopifyは操作対象のデータモデルでアルファベット順に並べたいので
#{名詞}#{動詞}
になってるけど、多くのアプリケーションでは操作されるデータとアクション名が対になることが少ない。 - 例えば
sendPasswordResetEmail
というmutationは操作対象というよりはRPC(Remote Procedure Call)的な発想なので動詞が先に来て欲しい。- Shopifyよりもこっちのほうが賛成できるな〜〜〜〜
- Shopifyは操作対象のデータモデルでアルファベット順に並べたいので
Specificity
- mutationは汎化させて引数でどうこうするよりも特定のケースごとに作ったほうが良い。
- 例えば
sendEmail(type: PASSWORD_RESET)
みたいなmutationは未来の機能拡張によっては引数や返り値が複雑になりうるのでsendPasswordResetEmail
として言い切ってしまったほうが良い。Specific mutations that correspond to semantic user actions are more powerful than general mutations
- とあるのでめっちゃ強い思いを持ってるっぽい
- 例えば
Nesting
input object
- inputは必ず1つのnon-nullかつmutationごとにユニークなinput objectで渡されるべき。要はこういうこと↓
# Good updatePost(input: { id: 4, newText: "..." }) { ... } # Not Good updatePost(id: 4, newText: "...") { ... }
- 小さい違いだけれど、引数の数が多くなった時にGraphQLのmutation stringがかなり違ってくる↓
mutation MyMutation($input: UpdatePostInput!) { updatePost(input: $input) { ... } } # vs. mutation MyMutation($id: ID!, $newText: String, ...) { updatePost(id: $id, newText: $newText, ...) { ... } }
- input objectは可能な限りネストしたほうがいい。
- ネストすると将来的に不必要になったフィールドだけ
deprecated
を付けたりして別のフィールドを置いたり出来る。
- ネストすると将来的に不必要になったフィールドだけ
mutation { createPerson(input: { # By nesting we have room at the top level of `input` # to add fields like `password`, or metadata fields like # `clientMutationId` for Relay. We could also deprecate # `person` in the future to use another top level field # like `partialPerson`. password: "qwerty" person: { id: 4 name: "Budd Deey" } }) { ... } updatePerson(input: { # The `id` field represents who we want to update. id: 4 # The `patch` field represents what we want to update. patch: { name: "Budd Deey" } }) { ... } }
- そうなのか〜〜〜
- mutation stringの長さが短くなるのはそうなんだけど、結局mutation stringの中でinputから取り出すのは同じだからそこまで短くなるんだっけ?とは思ったけどまぁdeprecatedにしやすいってのは納得した
mutation payload
- mutation payloadに関してもinputと同じことが言えて、ネストするのが望ましい。
mutation {
createPerson(input: { ... }) {
# You could add other fields now to your mutation payload.
# Like `clientMutationId` or `userErrors`.
person {
id
name
}
}
updatePerson(input: { ... }) {
person {
id
name
}
}
}
- 要するにこういう風に書いて、いきなりnameとかidとかのプリミティブな型を置くなという話
- >Preemptively removing design space is not something you want to do when designing a versionless GraphQL API.
- 金言だなぁ