├── Gemfile ├── Gemfile.lock ├── bad_schema └── schema.rb ├── good_schema ├── find_loader.rb ├── foreign_key_loader.rb └── schema.rb ├── readme.md ├── run_query.rb └── support ├── app.rb ├── query_logger.rb └── seeds.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | gem "graphql" 5 | gem "graphql-batch" 6 | gem "sequel" 7 | gem "sqlite3" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | graphql (0.13.0) 5 | graphql-batch (0.2.1) 6 | graphql (~> 0.8) 7 | promise.rb (~> 0.7.0.rc2) 8 | promise.rb (0.7.0) 9 | sequel (4.34.0) 10 | sqlite3 (1.3.11) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | graphql 17 | graphql-batch 18 | sequel 19 | sqlite3 20 | 21 | BUNDLED WITH 22 | 1.11.2 23 | -------------------------------------------------------------------------------- /bad_schema/schema.rb: -------------------------------------------------------------------------------- 1 | require "graphql" 2 | 3 | ArtistType = GraphQL::ObjectType.define do 4 | name("Artist") 5 | field :name, types.String 6 | end 7 | 8 | CardType = GraphQL::ObjectType.define do 9 | name("Card") 10 | field :name, types.String 11 | field :expansion, -> { ExpansionType } 12 | end 13 | 14 | ExpansionType = GraphQL::ObjectType.define do 15 | name("Expansion") 16 | field :name, types.String 17 | field :cards, -> { types[CardType] } 18 | field :artists, -> { types[ArtistType] } 19 | end 20 | 21 | QueryType = GraphQL::ObjectType.define do 22 | field :card, CardType do 23 | argument :id, !types.Int 24 | resolve -> (obj, args, ctx) { 25 | MTG::Card.find(id: args[:id]) 26 | } 27 | end 28 | end 29 | 30 | module MTG 31 | Schema = GraphQL::Schema.new(query: QueryType) 32 | end 33 | -------------------------------------------------------------------------------- /good_schema/find_loader.rb: -------------------------------------------------------------------------------- 1 | # Load instances of the same class by ID. 2 | # 3 | # Instead of: 4 | # - SELECT * FROM cards WHERE id=1; 5 | # - SELECT * FROM cards WHERE id=2; 6 | # - SELECT * FROM cards WHERE id=3; 7 | # 8 | # Execute: 9 | # - SELECT * FROM cards WHERE id IN(1,2,3); 10 | class FindLoader < GraphQL::Batch::Loader 11 | def initialize(model) 12 | @model = model 13 | end 14 | 15 | def perform(ids) 16 | records = @model.where(id: ids.uniq) 17 | records.each { |record| fulfill(record.id, record) } 18 | # If a record wasnt found, fulfill with nil: 19 | ids.each { |id| fulfill(id, nil) unless fulfilled?(id) } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /good_schema/foreign_key_loader.rb: -------------------------------------------------------------------------------- 1 | class ForeignKeyLoader < GraphQL::Batch::Loader 2 | def initialize(model, foreign_key) 3 | @model = model 4 | @foreign_key = foreign_key 5 | end 6 | 7 | def perform(foreign_value_sets) 8 | foreign_values = foreign_value_sets.flatten.uniq 9 | records = @model.where(@foreign_key => foreign_values).to_a 10 | 11 | foreign_value_sets.each do |foreign_value_set| 12 | matching_records = records.select { |r| foreign_value_set.include?(r.send(@foreign_key)) } 13 | fulfill(foreign_value_set, matching_records) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /good_schema/schema.rb: -------------------------------------------------------------------------------- 1 | require "graphql" 2 | require "graphql/batch" 3 | require_relative "./find_loader" 4 | require_relative "./foreign_key_loader" 5 | 6 | ArtistType = GraphQL::ObjectType.define do 7 | name("Artist") 8 | field :name, types.String 9 | end 10 | 11 | CardType = GraphQL::ObjectType.define do 12 | name("Card") 13 | field :name, types.String 14 | field :expansion, -> { ExpansionType } do 15 | resolve -> (obj, args, ctx) { 16 | FindLoader.for(MTG::Expansion).load(obj.expansion_id) 17 | } 18 | end 19 | end 20 | 21 | ExpansionType = GraphQL::ObjectType.define do 22 | name("Expansion") 23 | field :name, types.String 24 | field :cards, -> { types[CardType] } do 25 | resolve -> (obj, args, ctx) { 26 | ForeignKeyLoader.for(MTG::Card, :expansion_id).load([obj.id]) 27 | } 28 | end 29 | field :artists, -> { types[ArtistType] } do 30 | resolve -> (obj, args, ctx) { 31 | ForeignKeyLoader.for(MTG::Card, :expansion_id).load([obj.id]).then do |cards| 32 | ForeignKeyLoader.for(MTG::Artist, :id).load(cards.map(&:artist_id)) 33 | end 34 | } 35 | end 36 | end 37 | 38 | 39 | QueryType = GraphQL::ObjectType.define do 40 | field :card, CardType do 41 | argument :id, !types.Int 42 | resolve -> (obj, args, ctx) { 43 | FindLoader.for(MTG::Card).load(args[:id]) 44 | } 45 | end 46 | end 47 | 48 | module MTG 49 | Schema = GraphQL::Schema.new(query: QueryType) 50 | Schema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy 51 | end 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GraphQL::Batch Example 2 | 3 | This is a "before & after" example of [`graphql-batch`](https://github.com/shopify/graphql-batch). 4 | 5 | It logs SQL queries during query execution. 6 | 7 | ## Before 8 | 9 | ``` 10 | ~/projects/graphql-batch-example $ bundle exec ruby run_query.rb bad 11 | I, [2016-01-24T22:49:25.537066 #92405] INFO -- : (0.000120s) SELECT * FROM `cards` WHERE (`id` = 1) LIMIT 1 12 | I, [2016-01-24T22:49:25.537388 #92405] INFO -- : (0.000093s) SELECT * FROM `expansions` WHERE `id` = 2 13 | I, [2016-01-24T22:49:25.537915 #92405] INFO -- : (0.000122s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 2) 14 | I, [2016-01-24T22:49:25.538630 #92405] INFO -- : (0.000107s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 2) 15 | I, [2016-01-24T22:49:25.538997 #92405] INFO -- : (0.000076s) SELECT * FROM `cards` WHERE (`id` = 2) LIMIT 1 16 | I, [2016-01-24T22:49:25.539237 #92405] INFO -- : (0.000085s) SELECT * FROM `expansions` WHERE `id` = 2 17 | I, [2016-01-24T22:49:25.539504 #92405] INFO -- : (0.000094s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 2) 18 | I, [2016-01-24T22:49:25.539871 #92405] INFO -- : (0.000099s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 2) 19 | I, [2016-01-24T22:49:25.540216 #92405] INFO -- : (0.000078s) SELECT * FROM `cards` WHERE (`id` = 3) LIMIT 1 20 | I, [2016-01-24T22:49:25.540445 #92405] INFO -- : (0.000072s) SELECT * FROM `expansions` WHERE `id` = 3 21 | I, [2016-01-24T22:49:25.540727 #92405] INFO -- : (0.000094s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 3) 22 | I, [2016-01-24T22:49:25.541057 #92405] INFO -- : (0.000089s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 3) 23 | I, [2016-01-24T22:49:25.541388 #92405] INFO -- : (0.000074s) SELECT * FROM `cards` WHERE (`id` = 4) LIMIT 1 24 | I, [2016-01-24T22:49:25.541578 #92405] INFO -- : (0.000068s) SELECT * FROM `expansions` WHERE `id` = 1 25 | I, [2016-01-24T22:49:25.541839 #92405] INFO -- : (0.000083s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 1) 26 | I, [2016-01-24T22:49:25.542128 #92405] INFO -- : (0.000083s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 1) 27 | I, [2016-01-24T22:49:25.542448 #92405] INFO -- : (0.000077s) SELECT * FROM `cards` WHERE (`id` = 5) LIMIT 1 28 | I, [2016-01-24T22:49:25.542667 #92405] INFO -- : (0.000070s) SELECT * FROM `expansions` WHERE `id` = 2 29 | I, [2016-01-24T22:49:25.542940 #92405] INFO -- : (0.000097s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 2) 30 | I, [2016-01-24T22:49:25.543285 #92405] INFO -- : (0.000092s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 2) 31 | I, [2016-01-24T22:49:25.543568 #92405] INFO -- : (0.000067s) SELECT * FROM `cards` WHERE (`id` = 6) LIMIT 1 32 | I, [2016-01-24T22:49:25.543750 #92405] INFO -- : (0.000064s) SELECT * FROM `expansions` WHERE `id` = 2 33 | I, [2016-01-24T22:49:25.544019 #92405] INFO -- : (0.000095s) SELECT * FROM `cards` WHERE (`cards`.`expansion_id` = 2) 34 | I, [2016-01-24T22:49:25.544357 #92405] INFO -- : (0.000091s) SELECT `artists`.* FROM `artists` INNER JOIN `cards` ON (`cards`.`artist_id` = `artists`.`id`) WHERE (`cards`.`expansion_id` = 2) 35 | ``` 36 | 37 | ## After 38 | 39 | ``` 40 | ~/projects/graphql-batch-example $ bundle exec ruby run_query.rb good 41 | I, [2016-01-24T22:49:55.412548 #92430] INFO -- : (0.000234s) SELECT * FROM `cards` WHERE (`id` IN (1, 2, 3, 4, 5, 6)) 42 | I, [2016-01-24T22:49:55.413479 #92430] INFO -- : (0.000172s) SELECT * FROM `expansions` WHERE (`id` IN (2, 3, 1)) 43 | I, [2016-01-24T22:49:55.414457 #92430] INFO -- : (0.000190s) SELECT * FROM `cards` WHERE (`expansion_id` IN (2, 3, 1)) 44 | I, [2016-01-24T22:49:55.415565 #92430] INFO -- : (0.000178s) SELECT * FROM `artists` WHERE (`id` IN (2, 3, 1)) 45 | ``` 46 | 47 | ## 💰 48 | -------------------------------------------------------------------------------- /run_query.rb: -------------------------------------------------------------------------------- 1 | require_relative "./support/seeds" 2 | require 'pp' 3 | 4 | if ARGV[0] == "bad" 5 | require_relative "./bad_schema/schema" 6 | elsif ARGV[0] == "good" 7 | require_relative "./good_schema/schema" 8 | else 9 | raise("Pass `good` or `bad` to demonstrate a schema") 10 | end 11 | 12 | MTG::LOGGER.quiet = false 13 | 14 | query_string = %| 15 | query getCard { 16 | card_1: card(id: 1) { ... cardFields } 17 | card_2: card(id: 2) { ... cardFields } 18 | card_3: card(id: 3) { ... cardFields } 19 | card_4: card(id: 4) { ... cardFields } 20 | card_5: card(id: 5) { ... cardFields } 21 | card_6: card(id: 6) { ... cardFields } 22 | } 23 | fragment cardFields on Card { 24 | expansion { 25 | name 26 | cards { 27 | name 28 | } 29 | artists { 30 | name 31 | } 32 | } 33 | } 34 | | 35 | 36 | result = MTG::Schema.execute(query_string) 37 | # If you want to see the result: 38 | pp result 39 | -------------------------------------------------------------------------------- /support/app.rb: -------------------------------------------------------------------------------- 1 | require "sequel" 2 | require_relative "./query_logger" 3 | module MTG 4 | LOGGER = TestLogger.new 5 | LOGGER.quiet = true 6 | DB = Sequel.sqlite '', :loggers => [LOGGER] 7 | 8 | DB.create_table(:expansions) do 9 | primary_key :id 10 | string :name 11 | date :released_on 12 | end 13 | 14 | DB.create_table(:artists) do 15 | primary_key :id 16 | string :name 17 | end 18 | 19 | DB.create_table(:cards) do 20 | primary_key :id 21 | string :name 22 | foreign_key :expansion_id, :expansions 23 | foreign_key :artist_id, :artists 24 | end 25 | 26 | class Expansion < Sequel::Model 27 | one_to_many :cards 28 | many_to_many :artists, join_table: :cards, uniq: true 29 | end 30 | 31 | class Artist < Sequel::Model 32 | one_to_many :cards 33 | many_to_many :expansions, join_table: :cards, uniq: true 34 | end 35 | 36 | class Card < Sequel::Model 37 | many_to_one :artist 38 | many_to_one :expansion 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /support/query_logger.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | # Like a normal logger, but you can tell it to be quiet sometimes 4 | class TestLogger 5 | attr_accessor :quiet 6 | def initialize 7 | @logger = Logger.new($stdout) 8 | @messages = [] 9 | self.quiet = false 10 | end 11 | 12 | def method_missing(method_name, *args, &block) 13 | @messages << args 14 | if !quiet 15 | @logger.send(method_name, *args, &block) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /support/seeds.rb: -------------------------------------------------------------------------------- 1 | require_relative "./app" 2 | 3 | THOMAS_BAXA = MTG::Artist.create(name: "Thomas Baxa") 4 | ZOLTAN_BOROS = MTG::Artist.create(name: "Zoltan Boros") 5 | WAYNE_REYNOLDS = MTG::Artist.create(name: "Wayne Reynolds") 6 | 7 | SHARDS = MTG::Expansion.create(name: "Shards of Alara") 8 | CONFLUX = MTG::Expansion.create(name: "Conflux") 9 | REBORN = MTG::Expansion.create(name: "Alara Reborn") 10 | 11 | MTG::Card.create(name: "Volcanic Fallout", artist: ZOLTAN_BOROS, expansion: CONFLUX) 12 | MTG::Card.create(name: "Ignite Disorder", artist: ZOLTAN_BOROS, expansion: CONFLUX) 13 | MTG::Card.create(name: "Wildfield Borderpost", artist: ZOLTAN_BOROS, expansion: REBORN) 14 | MTG::Card.create(name: "Ajani Vengeant", artist: WAYNE_REYNOLDS, expansion: SHARDS) 15 | MTG::Card.create(name: "Madrush Cyclops", artist: WAYNE_REYNOLDS, expansion: CONFLUX) 16 | MTG::Card.create(name: "Wall of Reverence", artist: WAYNE_REYNOLDS, expansion: CONFLUX) 17 | MTG::Card.create(name: "Blightning", artist: THOMAS_BAXA, expansion: SHARDS) 18 | MTG::Card.create(name: "Identity Crisis", artist: THOMAS_BAXA, expansion: REBORN) 19 | MTG::Card.create(name: "Necromancer's Covenant", artist: THOMAS_BAXA, expansion: REBORN) 20 | --------------------------------------------------------------------------------