Save 37% off PRO during our Black Friday Sale! »

Что скрывает «под капотом» GraphQL Ruby, и как это помогает писать более элегантный API

Что скрывает «под капотом» GraphQL Ruby, и как это помогает писать более элегантный API

Разобраться в большом open-source проекте не так то просто: большой объем исходного кода и обилие неочевидных решений, появившихся в процессе эволюции проекта повышают порог входа. В докладе я рассказажу о том, что происходит, когда объявляется новый тип, а также о том, как происходит обработка запроса на эндпоинт /graphql. Эти знания помогут нам понять, как работают плагины для graphql-ruby и даже написать свое собственное несложное расширение!
Доклад будет полезен:

тем, кто работает с graphql-ruby, и хочет знать, что там внутри
тем, кто хочет понять, как реализуется среда исполнения GraphQL
тем, кто хочет научиться разбираться с устройством популярных open-source библиотек
тем, кто хочет научиться контрибутить в open-source

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

September 28, 2019
Tweet

Transcript

  1. graphql-ruby under the hood and how to write more elegant

    APIs Dmitry Tsepelev, Evil Martians
  2. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 2

  3. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 3

  4. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

  5. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 5 evilmartians.com

  6. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 6 uses graphql-ruby and wants to

    know how it works internally wants to understand, how GraphQL execution engines are implemented wants to extend the API of graphql-ruby wants to learn, how to dig into a popular open-source project and start contributing THIS TALK IS FOR PEOPLE, WHO
  7. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 7 GraphQL query language how to

    use GraphQL in your projects pros and cons of GraphQL as a technology WHAT WE ARE NOT GOING TO DISCUSS
  8. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 8 DUMP 2019 Saint P Rubyconf

    2019 https:!//youtu.be/xUrLslKdnr8 https:!//youtu.be/CjOwKbf8L3I?t=9615
  9. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 9 https:!//evl.ms/blog/graphql-on-rails-1-from-zero-to-the-first-query

  10. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 10 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER

    THE HOOD EXTENDING LIBRARY API
  11. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 11 every entity is a type

    each type has a list of fields some types are connected DATA AS A GRAPH
  12. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 12 Request: query { user(id: 42)

    { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } } QUERY LANGUAGE
  13. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 13 query language execution engine WHAT

    IS GRAPHQL?
  14. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 14 type UserType { id: ID!

    name: String! orders: [OrderType]! } SCHEMA
  15. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 15 gem serializes the data you

    provide implements execution engine rules WHAT IS GRAPHQL-RUBY?
  16. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 16 $ rails generate graphql:install route

    post "/graphql", to: "graphql#execute" GraphqlController app/graphql/ contains some base classes
  17. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 17 CONTROLLER AND SCHEMA class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], context: {}, operation_name: params[:operationName] ) render json: result end end class GraphqlSchema < GraphQL!::Schema query Types!::QueryType end
  18. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 18 ROOT FIELDS class Types!::QueryType <

    GraphQL!::Schema!::Object field :user, Types!::UserType, null: true do argument :id, ID, required: true end def user(id:) User.find(id) end end
  19. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 19 TYPE DEFINITION class Types!::UserType <

    GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end
  20. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 20 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER

    THE HOOD EXTENDING LIBRARY API
  21. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev SCHEMA DEFINITION 21

  22. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 22 GraphQL::Schema class GraphQL!::Schema class !<<

    self def query(query_type) @query_type = query_type end end end class GraphqlSchema < GraphQL!::Schema query Types!::QueryType end application code library code
  23. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 23 class GraphqlSchema < GraphQL!::Schema lazy_resolve

    OrdersResolver, :orders end class Types!::UserType < GraphQL!::Schema!::Object field :orders, [OrderType], null: false def orders OrdersResolver.new(context, object.id) end end GraphQL::Schema#lazy_resolve
  24. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 24 class OrdersResolver def initialize(query_ctx, user_id)

    @user_id = user_id @lazy_state = query_ctx[:lazy_find_orders] !!||= { pending_ids: Set.new, loaded_ids: {} } @lazy_state[:pending_ids] !<< user_id end def orders loaded_records = @lazy_state[:loaded_ids][@user_id] return loaded_records if loaded_records pending_ids = @lazy_state[:pending_ids].to_a @lazy_state[:loaded_ids] = Order.where(user_id: pending_ids).group_by(&:user_id) @lazy_state[:pending_ids].clear @lazy_state[:loaded_ids][@user_id] end end GraphQL::Schema#lazy_resolve
  25. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Schema class !<< self def

    lazy_resolve(lazy_class, value_method) lazy_classes[lazy_class] = value_method end def lazy_classes @lazy_classes !!||= {} end end end GraphQL::Schema#lazy_resolve 25
  26. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 26 GraphQL::Schema::Object class GraphQL!::Schema!::Object class !<<

    self def field(*args, !**kwargs, &block) own_fields[field_defn.name] = field_class.from_options( *args, !**kwargs, &block ) end def own_fields @own_fields !!||= {} end end end class Types!::UserType field :id, ID, null: false end application code library code
  27. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 27 GraphQL::Schema::Object class GraphQL!::Schema!::Object class !<<

    self def field(*args, !**kwargs, &block) own_fields[field_defn.name] = GraphQL!::Schema!::Field.from_options( *args, !**kwargs, &block ) end def own_fields @own_fields !!||= {} end end end class Types!::UserType field :id, ID, null: false end application code library code
  28. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field 28 class GraphQL!::Schema!::Field def self.from_options(name

    = nil, …, !**kwargs, &block) kwargs[:name] = name if name new(!**kwargs, &block) end attr_accessor :name def initialize(…, &definition_block) @name = name @type = type @null = null … end end
  29. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev &definition_block 29 class Types!::QueryType < GraphQL!::Schema!::Object

    field :user, Types!::UserType, null: true do argument :id, ID, required: true end end
  30. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 30 GraphQL::Schema::Field class GraphQL!::Schema!::Field def initialize(!!...,

    &definition_block) instance_eval(&definition_block) if definition_block end def argument(*args, !**kwargs, &block) arg_defn = GraphQL!::Schema!::Argument.new(*args, !**kwargs, &block) own_arguments[arg_defn.name] = arg_defn end def own_arguments @own_arguments !!||= {} end end
  31. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 31 #<GraphQL!::Schema!::Field @name="name", @resolver_class=nil, @resolver_method=:name >

    FIELD DATA STRUCTURE class Types!::UserType < GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end Types!::UserType.fields
  32. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 32 #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver

    @own_arguments= { "page"!=> #<Argument @name="page", @null=true, @type_expr=Integer>, "perPage"!=> #<Argument @name="perPage", @null=true, @type_expr=Integer> } > class Types!::UserType < GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end Types!::UserType.fields FIELD DATA STRUCTURE
  33. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev RESOLVING QUERIES 33

  34. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema#execute class GraphQL!::Schema def self.execute(query_str =

    nil, !**kwargs) kwargs[:query] = query_str if query_str all_results = GraphQL!::Execution!::Multiplex.run_all(self, [kwargs]) all_results[0] end end 34
  35. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex class !<< self def

    run_all(schema, query_options, *args) queries = query_options.map do |opts| GraphQL!::Query.new(schema, nil, opts) end run_queries(schema, queries, *args) end end end 35 Schema.execute ⇨ Multiplex.run_all GraphQL::Execution::Multiplex.run_all
  36. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Query#prepare_ast GraphQL!::Language!::Parser.parse !!<<-QUERY query { user

    { name } } QUERY 36 query user name
  37. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev @operation_type= @name= @name= GraphQL::Query#prepare_ast 37 query

    user name Document OperationDefinition Field Field @selections @selections @definitions
  38. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex class !<< self def

    run_all(schema, query_options, *args) queries = query_options.map do |opts| GraphQL!::Query.new(schema, nil, opts) end run_queries(schema, queries, *args) end end end GraphQL::Execution::Multiplex.run_all 38 Schema.execute ⇨ Multiplex.run_all
  39. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Execution::Multiplex#run_as_multiplex class GraphQL!::Execution!::Multiplex def run_as_multiplex(multiplex) strategy

    = multiplex.schema.query_execution_strategy results = multiplex.queries.map do |query| strategy.begin_query(query, multiplex) end results.each_with_index.map do |data_result, idx| multiplex.queries[idx].result end end end 39 Schema.execute Multiplex.run_all … ⇨ Multiplex#run_as_multiplex
  40. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Execution::Execute 40 class GraphQL!::Execution!::Execute def self.begin_query(query,

    _multiplex) ExecutionFunctions.resolve_root_selection(query) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex ⇨ Execute.begin_query
  41. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_root_selection 41 module ExecutionFunctions def resolve_root_selection(query)

    operation = query.selected_operation root_type = query.root_type_for_operation( operation.operation_type ) resolve_selection( query.root_value, root_type, query.context ) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ⇨ ExecutionFunctions.resolve_root_selection
  42. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_selection 42 module ExecutionFunctions def resolve_selection(object,

    current_type, current_ctx) current_ctx.value = {} selections_on_type = current_ctx.irep_node .typed_children[current_type] selections_on_type.each do |name| field_ctx = current_ctx.spawn_child(!!...) resolve_field(object, field_ctx) current_ctx.value[name] = field_ctx end current_ctx.value end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ⇨ ExecutionFunctions.resolve_selection resolve_selection
  43. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev module ExecutionFunctions def resolve_field(object, field_ctx) raw_value

    = field_ctx.schema .middleware .invoke(object, field_ctx) field_ctx.value = resolve_value( raw_value, field_ctx.type, field_ctx ) end end ExecutionFunctions.resolve_field 43 Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ⇨ ExecutionFunctions.resolve_field resolve_field
  44. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 44

  45. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev FieldResolveStep 45 module FieldResolveStep def self.call(

    _parent_type, parent_object, field_definition, field_args, context ) field_definition.resolve( parent_object, field_args, context ) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … ⇨ FieldResolverStep.call
  46. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 46 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … FieldResolverStep.call ⇨ Field#resolve resolve
  47. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 47 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver @resolver_method=:resolve >
  48. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 48 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver @resolver_method=:resolve >
  49. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 49 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver @resolver_method=:resolve >
  50. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 50 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) field_receiver.public_send(@resolver_method, !**e_args) end end end resolve #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver @resolver_method=:resolve >
  51. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 51 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) field_receiver.public_send(:resolve, !**e_args) end end end resolve #<GraphQL!::Schema!::Field @method_str="orders", @method_sym=:orders, @name="orders", @resolver_class=OrderResolver @resolver_method=:resolve >
  52. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 52 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="name", @method_sym=:name, @name="name", @resolver_class=nil, @resolver_method=:name >
  53. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 53 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="name", @method_sym=:name, @name="name", @resolver_class=nil, @resolver_method=:name >
  54. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 54 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #<GraphQL!::Schema!::Field @method_str="name", @method_sym=:name, @name="name", @resolver_class=nil, @resolver_method=:name >
  55. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 55 class GraphQL!::Schema!::Field def resolve(object,

    args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj field_receiver.public_send(:name, !**e_args) end end end resolve #<GraphQL!::Schema!::Field @method_str="name", @method_sym=:name, @name="name", @resolver_class=nil, @resolver_method=:name >
  56. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev module ExecutionFunctions def resolve_field(object, field_ctx) raw_value

    = field_ctx.schema .middleware .invoke(object, field_ctx) field_ctx.value = resolve_value( raw_value, field_ctx.type, field_ctx ) end end ExecutionFunctions.resolve_field 56 Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ⇨ ExecutionFunctions.resolve_field
  57. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_value 57 module ExecutionFunctions def resolve_value(value,

    field_type, field_ctx) field_defn = field_ctx.field case field_type.kind when GraphQL!::TypeKinds!::SCALAR field_type.coerce_result(value, field_ctx) when GraphQL!::TypeKinds!::OBJECT resolve_selection(value, field_type, field_ctx) end end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … ⇨ ExecutionFunctions.resolve_value resolve_value
  58. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex def run_as_multiplex(multiplex) strategy =

    multiplex.schema.query_execution_strategy results = multiplex.queries.map do |query| strategy.begin_query(query, multiplex) end results.each_with_index.map do |data_result, idx| multiplex.queries[idx].result end end end GraphQL::Execution::Multiplex#run_as_multiplex 58 Schema.execute Multiplex.run_all … ⇨ Multiplex#run_as_multiplex
  59. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev IS IT SLOW? 59 https:!//gist.github.com/DmitryTsepelev/36e290cf64b4ec0b18294d0a57fb26ff

  60. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 60 GRAPHQL PARSER BENCHMARK fields –

    count of fields in each selection set nested levels – count of nested levels query { field1 { field1 field2 } field2 { field1 field2 } } fields = 2, nested levels = 1
  61. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 61 user system total real 1

    fields, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152) 1 fields, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187) 1 fields, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240) 2 fields, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132) 2 fields, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411) 2 fields, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694) 4 fields, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153) 4 fields, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744) 4 fields, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581) 8 fields, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352) 8 fields, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917) 8 fields, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507) GRAPHQL PARSER BENCHMARK
  62. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 62 30.50 kB (285 objects) /users/1?include=orders&fields[users]=id,name&fields[orders]=id,placed_at

    { "data" !=> { "id"!=>"1", "type"!=>"users", "links"!=>{"self"!=>"users/1"}, "attributes"!=>{"id"!=>"1", "name"!=>"John Doe"} }, "included"!=> [ { "id"!=>"1", "type"!=>"orders", "links"!=>{"self"!=>"orders/1"}, "attributes"!=>{"id"!=>1, "placed_at"!=>"2019-09-21T12:56:31+03:00"} }, { "id"!=>"2", "type"!=>"orders", "links"!=>{"self"!=>"orders/2"}, "attributes"!=>{"id"!=>2, "placed_at"!=>"2019-09-21T12:56:31+03:00"} } ] } MEMORY PROFILER (JSON API)
  63. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 63 113.19 kB (1059 objects) query

    GetUser($id: ID!) { user(id: $id) { id name orders { id placedAt } } } { "data" !=> { "user" !=> { "id"!=>"1", "name"!=>"John Doe", "orders"!=>[ { "id"!=>"1", "placedAt"!=>"2019-09-21T13:00:02+03:00" }, { "id"!=>"2", "placedAt"!=>"2019-09-21T13:00:02+03:00" } ] } } } MEMORY PROFILER (GRAPHQL)
  64. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 64 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER

    THE HOOD EXTENDING LIBRARY API
  65. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 65 BLOCK-BASED API Types!::Post = GraphQL!::ObjectType.define

    do field :comments, types[Types!::Comments] do argument :orderBy, Types!::CommentOrder resolve !->(obj, args, ctx) { obj.comments.order(args[:orderBy]) } end end
  66. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 66 looks more like JavaScript than

    Ruby problems with constant loading (especially in Rails) hard to patch https:!//rmosolgo.github.io/blog/2018/03/25/why-a-new-schema-definition-api/ BLOCK-BASED API
  67. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 67 graphql-ruby supports both APIs some

    classes look more complex than they could some old plugins do not work BLOCK-BASED API
  68. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GRAPHQL-BATCH 68

  69. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 69 class GraphqlSchema < GraphQL!::Schema query

    QueryType use GraphQL!::Batch end class QueryType < GraphQL!::Schema!::Object field :product_title, String, null: true do argument :id, ID, required: true end def product_title(id:) RecordLoader.for(Product).load(id).then(&:title) end end SETUP AND USAGE
  70. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 70 class RecordLoader < GraphQL!::Batch!::Loader def

    initialize(model) @model = model end def perform(ids) @model.where(id: ids).each do |record| fulfill(record.id, record) end ids.each do |id| fulfill(id, nil) unless fulfilled?(id) end end end LOADING DATA
  71. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Schema class !<< self def

    use(plugin, options = {}) plugins !<< [plugin, options] end def plugins @plugins !!||= [] end def to_graphql plugins.each { |plugin| plugin.use(self) } end end end 71 HOW SCHEMA LOADS PLUGINS
  72. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 72 PATCHING SCHEMA WITH #use module

    GraphQL"::Batch def self.use(schema_defn, executor_class: GraphQL!::Batch!::Executor) instrumentation = GraphQL!::Batch!::SetupMultiplex.new( schema, executor_class: executor_class ) schema_defn.instrument(:multiplex, instrumentation) schema_defn.lazy_resolve(!::Promise, :sync) end end
  73. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 73 REDEFINING FIELD RESOLVER class GraphQL!::Batch!::SetupMultiplex

    def instrument(type, field) old_resolve_proc = field.resolve_proc field.redefine do resolve !->(obj, args, ctx) { !::Promise.sync(old_resolve_proc.call(obj, args, ctx)) } end end end
  74. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev FIELD EXTENSIONS 74

  75. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev HOW TO EXTEND A FIELD 75

    class Types!::QueryType < GraphQL!::Schema!::Object field :users, [Types!::UserType], null: false, extensions: [SearchableExtension] def users(query:) User.search(query) end end
  76. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ADDING ARGUMENT TO A FIELD USING

    EXTENSION 76 class SearchableExtension < GraphQL!::Schema!::FieldExtension def apply field.argument(:query, String, required: false) end end
  77. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev HOW EXTENSIONS WORK 77 class GraphQL!::Schema!::Field

    def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| # resolve logic end end def with_extensions(obj, args, ctx) extensions.each do |extension| obj, args = extension.resolve( object: obj, arguments: args, context: ctx ) end yield(obj, args) end end
  78. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev PLUGIN IN 5 MINUTES ⏰ 78

    https:!//gist.github.com/DmitryTsepelev/065bb6bc796898f5745c4209d1b4bb21
  79. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 79 WHAT WE ARE BUILDING class

    QueryType < BaseObject field :cached_val, String, null: false def cached_val "I'm cached at "#{Time.now}" end end
  80. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 80 ADDING EXTENSION TO THE FIELD

    class QueryType < BaseObject field :cached_val, String, null: false, extensions: [CacheExtension] def cached_val "I'm cached at "#{Time.now}" end end
  81. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 81 HOW TO IMPLEMENT THE EXTENSION

    class CacheExtension < GraphQL!::Schema!::FieldExtension def resolve(object:, arguments:, !**rest) key = cache_key(object, arguments) store[key] !!||= yield(object, arguments) end private def store Thread.current[:field_cache] !!||= {} end def cache_key(object, arguments) ""#{object.class.name}-"#{@field}-"#{arguments.hash}" end end
  82. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 82 I WANT TO USE KWARG!

    class QueryType < BaseObject field :cached_val, String, null: false, cached: true def cached_val "I'm cached at "#{Time.now}" end end
  83. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 83 PATCHING BASE CLASS class BaseObject

    < GraphQL!::Schema!::Object field_class.prepend(Module.new do def initialize(*args, !**kwargs, &block) if kwargs.delete(:cached) kwargs[:extensions] !!||= [] !<< CacheExtension end super end end) end
  84. RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 84 graphql-ruby is a complex library

    with a complex architecture this architecture allows to extend the functionality of the library benefits coming with GraphQL do not require to sacrifice the performance TO SUM UP
  85. evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians evl.ms/telegram THANK YOU! RUBYRUSSIA 2019 85