将来を見据えたウェブ・アプリケーションの構築:The Codestのエキスパート・チームによる洞察
The Codestが、最先端技術を駆使してスケーラブルでインタラクティブなウェブアプリケーションを作成し、あらゆるプラットフォームでシームレスなユーザー体験を提供することにどのように秀でているかをご覧ください。The Codestの専門知識がどのようにデジタルトランスフォーメーションとビジネス...
GraphQLには、他のテクノロジーと同様、問題があり、そのいくつかはアーキテクチャに直接起因し、いくつかは他のアプリケーションで見られるものと同じである。しかし、解決策はまったく異なります。
問題を提示するために、次のようなアプリケーション・アーキテクチャを想定してみよう:
対応するクエリは GraphQL をダウンロードしてください。システムに追加されたポスターとそのリンクとともに、すべてのリンクを取得します、
{
allLinks {
id
url
説明
作成日時
投稿日時
id
名前
リンク
id
}
}
}
下図のように、典型的なn + 1の問題がここにある。
リンクのロード (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
ユーザーロード (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ?LIMIT ? [["id", 40], ["LIMIT", 1] ]。
↳ app/controllers/graphql_controller.rb:5:in `execute'.
リンクロード (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]] とする。
↳ app/controllers/graphql_controller.rb:5:in `execute'
ユーザーロード (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ?LIMIT ? [["id", 38], ["LIMIT", 1] ]。
↳ app/controllers/graphql_controller.rb:5:in `execute'.
リンクのロード (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]] とする。
↳ app/controllers/graphql_controller.rb:5:in `execute'
ユーザーロード (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ?LIMIT ? [["id", 36], ["LIMIT", 1] ]。
↳ app/controllers/graphql_controller.rb:5:in `execute'
リンクロード (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]] とする。
↳ app/controllers/graphql_controller.rb:5:in `execute'
ユーザーロード (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ?LIMIT ? [["id", 34], ["LIMIT", 1] ]。
↳ app/controllers/graphql_controller.rb:5:in `execute'
リンクロード (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]] とする。
↳ app/controllers/graphql_controller.rb:5:in `execute'
ユーザーロード (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ?LIMIT ? [["id", 32], ["LIMIT", 1]] とする。
この場合、次のように動作する。 コード:Link.all.map(&:ユーザー).map(&:リンク)
.
私たちは問題の解決策を知っているようだ: Link.includes(ユーザー: :リンク).map(&:ユーザー).map(&:リンク)
しかし、本当に効果があるのだろうか?調べてみよう!
修正を確認するために GraphQL クエリーで、いくつかのフィールドのみを使用し、リレーションは使用しない。
{
allLinks {
id
url
説明
作成日時
}
}
残念ながら、この結果は、ユーザーとそのリンクに関連するリンクがないにもかかわらず、データベースのクエリにこのデータを添付していることを示している。残念なことに、これらは冗長であり、さらに複雑な構造を持つため、単純に非効率であることが判明した。
としてGraphqlController#executeで処理する。
パラメータ{"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}"", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}"}}
リンクロード (0.3ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
) [["id", 40], ["id", 38], ["id", 36], ["id", 34], ["id", 32], ["id", 30], ["id", 28], ["id", 26], ["id", 24], ["id"、22]、["id", 20]、["id", 18]、["id", 16]、["id", 14]、["id", 12]、["id", 10]、["id", 8]、["id", 6]、["id", 4]、["id", 2]] 。
↳ app/controllers/graphql_controller.rb:5:in `execute'
) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 16], ["user_id", 18], ["user_id", 20]、[user_id"、22]、["user_id"、24]、["user_id"、26]、["user_id"、28]、["user_id"、30]、["user_id"、32]、["user_id"、34]、["user_id"、36]、["user_id"、38]、["user_id"、40]]。
↳ app/controllers/graphql_controller.rb:5:in `execute'.
39ms で 200 OK を完了 (Views: 0.7ms | ActiveRecord: 0.9ms | Allocations: 8730)
で GraphQLしかし、このような問題は、クエリに入れるときにデータが必要であると仮定して、単純にデータを一括してロードすることで解決される。これが遅延ロードである。最もポピュラーなライブラリのひとつがhttps://github.com/Shopify/graphql-batch/。
残念ながら、そのインストールは手間がかかる。データ・ローダーはこちらで入手できる。https://github.com/Shopify/graphql-batch/tree/master/examples、つまり レコードローダー
クラスとアソシエーションローダー
クラスをインストールしよう古典的に gem 'graphql-batch'
ライブラリを作成し、それをスキーマとローダーに追加する:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
使用 GraphQL::Batch
...
終了
そして私たちのタイプ:
# graphql-ruby/app/graphql/types/link_type.rb
モジュール Types
クラス LinkType < BaseNode
field :created_at, DateTimeType, null: false
field :url, String, null: false
field :description, String, null: false
field :posted_by, UserType, null: false, method: :user
field :votes, [Types::VoteType], null: false
def user
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
エンド
# graphql-ruby/app/graphql/types/user_type.rb
モジュール Types
クラス UserType < BaseNode
field :created_at, DateTimeType, null: false
field :name, String, null: false
field :email, String, null: false
field :votes, [VoteType], null: false
field :links, [LinkType], null: false
リンク
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
ローダーを使用した結果、データをバッチ化し、2つの単純なSQLクエリーでデータを照会することができる:
2021-06-16 22:40:17 +0200に::1のPOST "/graphql "を開始しました。
(0.1ms) SELECT sqlite_version(*)
としてGraphqlController#executeで処理。
パラメータ{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }}"}}".
リンクロード (0.4ms) SELECT "links".* FROM "links"
↳ app/controllers/graphql_controller.rb:5:in `execute'
) [["id", 2], ["id", 4], ["id", 6], ["id", 8], ["id", 10], ["id", 12], ["id", 14], ["id", 16], ["id", 18], ["id", 20]、[id", 22]、["id", 24]、["id", 26]、["id", 28]、["id", 30]、["id", 32]、["id", 34]、["id", 36]、["id", 38]、["id", 40]] 。
↳ app/graphql/loaders/record_loader.rb:12:in `perform'.
) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 16], ["user_id", 18], ["user_id", 20]、[user_id"、22]、["user_id"、24]、["user_id"、26]、["user_id"、28]、["user_id"、30]、["user_id"、32]、["user_id"、34]、["user_id"、36]、["user_id"、38]、["user_id"、40]]。
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'.
62ms で 200 OK を完了 (Views: 1.3ms | ActiveRecord: 1.8ms | Allocations: 39887)
この問題を解決する解決策は他にもある:
N+1クエリがすべてではない。 GraphQL の場合、次の属性を自由に引き継ぐことができます。デフォルトでは1に設定されています。これは、特にデータを自由にネストできる状況では、サーバーにとって大きすぎる場合があります。どのように対処すればよいのでしょうか?クエリの複雑さを制限することはできますが、そのためには、属性でコストを指定する必要があります。デフォルトでは1に設定されています。このコストは 複雑さだ:
属性にデータを入力することができる: field: links, [LinkType], null: false, complexity: 101
.制限を実際に機能させるのであれば、やはり上限を制度に導入する必要がある:
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
使用 GraphQL::Batch
最大複雑度 100
...
終了
GraphQL ローカルでできることと比較すると、トレースはそれほど単純ではありません。残念ながら、rack miniのプロファイラや通常のSQLログではすべてを知ることはできませんし、クエリのどの部分が特定のタイムスライスの原因なのかを特定することもできません。GraphQL-Rubyの場合、こちらで入手可能な商用ソリューションを利用することができる: https://graphql-ruby.org/queries/tracingあるいは、独自のトレースを用意することもできる。下のスニペットはローカル・トレーサーのようだ。
# lib/my_custom_tracer.rb
class MyCustomTracer 'graphql.lex'、
'parse' => 'graphql.parse'、
'validate' => 'graphql.validate'、
'analyze_query' => 'graphql.analyze_query'、
'analyze_multiplex' => 'graphql.analyze_multiplex'、
'execute_multiplex' => 'graphql.execute_multiplex'、
'execute_query' => 'graphql.execute_query'、
'execute_query_lazy' => 'graphql.execute_query_lazy'.
}
def platform_trace(platform_key, key, _data, &block)
start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC
result = block.call
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
observe(platform_key, key, duration)
結果
終了
def platform_field_key(type, field)
"graphql.#{type.graphql_name}.#{field.graphql_name}"
end
def platform_authorized_key(type)
"graphql.authorized.#{type.graphql_name}"
end
def platform_resolve_type_key(type)
"graphql.resolve_type.#{type.graphql_name}"
end
def observe(platform_key, key, duration)
return if key == 'authorized'
puts "platform_key: #{platform_key}, key: #{key}, duration:#{(duration * 1000).round(5)} ms".yellow
終了
終了
インストールも非常に簡単で、トレーサー情報をスキーマに含めるだけでよい。 トレーサー (MyCustomTracer.new)
コンフィギュレーションを使用します。以下の例のように:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
トレーサー(MyCustomTracer.new)
...
終了
このようなトレースからの出力は次のようになる:
2021-06-17 22:02:44 +0200に::1のPOST "/graphql "を開始しました。
(0.1ms) SELECT sqlite_version(*)
としてGraphqlController#executeで処理。
パラメータパラメータ:{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }}" }}。
platform_key: graphql.lex, key: lex, duration:0.156 ms
platform_key: graphql.parse, key: parse, duration:0.108 ms
platform_key: graphql.validate、キー: validate、継続時間: 0.537 ms0.537 ms
platform_key: graphql.analyze_query、キー: analyze_query、継続時間: 0.123 ms0.123 ms
platform_key: graphql.analyze_multiplex、キー: analyze_multiplex、継続時間: 0.159 ms0.159ms
リンクロード (0.4ms) SELECT "links".* FROM "links"
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, key: execute_query, duration:15.562 ms
↳ app/graphql/loaders/record_loader.rb:12:in `perform'.
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
platform_key: graphql.execute_query_lazy, key: execute_query_lazy, duration:14.12 ms
platform_key: graphql.execute_multiplex, key: execute_multiplex, duration:31.11 ms
200 OK を 48ms で完了 (Views: 1.2ms | ActiveRecord: 2.0ms | Allocations: 40128)
GraphQL はもう新しい技術ではないが、その問題に対する解決策は、図書館の一部でなければ完全には標準化されない。この技術を プロジェクト 個人的には、REST APIが提供するものとの関連で、これは新しい品質だと考えている。