1---
2stage: Enablement
3group: Database
4info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
5---
6
7# GraphQL BatchLoader
8
9GitLab uses the [batch-loader](https://github.com/exAspArk/batch-loader) Ruby gem to optimize and avoid N+1 SQL queries.
10
11It is the properties of the GraphQL query tree that create opportunities for batching like this - disconnected nodes might need the same data, but cannot know about themselves.
12
13## When should you use it?
14
15We should try to batch DB requests as much as possible during GraphQL **query** execution. There is no need to batch loading during **mutations** because they are executed serially. If you need to make a database query, and it is possible to combine two similar (but not identical) queries, then consider using the batch-loader.
16
17When implementing a new endpoint we should aim to minimise the number of SQL queries. For stability and scalability we must also ensure that our queries do not suffer from N+1 performance issues.
18
19## Implementation
20
21Batch loading is useful when a series of queries for inputs `Qα, Qβ, ... Qω` can be combined to a single query for `Q[α, β, ... ω]`. An example of this is lookups by ID, where we can find two users by usernames as cheaply as one, but real-world examples can be more complex.
22
23Batch loading is not suitable when the result sets have different sort-orders, grouping, aggregation or other non-composable features.
24
25There are two ways to use the batch-loader in your code. For simple ID lookups, use `::Gitlab::Graphql::Loaders::BatchModelLoader.new(model, id).find`. For more complex cases, you can use the batch API directly.
26
27For example, to load a `User` by `username`, we can add batching as follows:
28
29```ruby
30class UserResolver < BaseResolver
31  type UserType, null: true
32  argument :username, ::GraphQL::Types::String, required: true
33
34  def resolve(**args)
35    BatchLoader::GraphQL.for(username).batch do |usernames, loader|
36      User.by_username(usernames).each do |user|
37        loader.call(user.username, user)
38      end
39    end
40  end
41end
42```
43
44- `project_id` is the `ID` of the current project being queried
45- `loader.call` is used to map the result back to the input key (here a project ID)
46- `BatchLoader::GraphQL` returns a lazy object (suspended promise to fetch the data)
47
48Here an [example MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46549) illustrating how to use our `BatchLoading` mechanism.
49
50## How does it work exactly?
51
52Each lazy object knows which data it needs to load and how to batch the query. When we need to use the lazy objects (which we announce by calling `#sync`), they will be loaded along with all other similar objects in the current batch.
53
54Inside the block we execute a batch query for our items (`User`). After that, all we have to do is to call loader by passing an item which was used in `BatchLoader::GraphQL.for` method (`usernames`) and the loaded object itself (`user`):
55
56```ruby
57BatchLoader::GraphQL.for(username).batch do |usernames, loader|
58  User.by_username(usernames).each do |user|
59    loader.call(user.username, user)
60  end
61end
62```
63
64### What does lazy mean?
65
66It is important to avoid syncing batches too early. In the example below we can see how calling sync too early can eliminate opportunities for batching:
67
68```ruby
69x = find_lazy(1)
70y = find_lazy(2)
71
72# calling .sync will flush the current batch and will inhibit maximum laziness
73x.sync
74
75z = find_lazy(3)
76
77y.sync
78z.sync
79
80# => will run 2 queries
81```
82
83```ruby
84x = find_lazy(1)
85y = find_lazy(2)
86z = find_lazy(3)
87
88x.sync
89y.sync
90z.sync
91
92# => will run 1 query
93```
94
95## Testing
96
97Any GraphQL field that supports `BatchLoading` should be tested using the `batch_sync` method available in [GraphQLHelpers](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/helpers/graphql_helpers.rb).
98
99```ruby
100it 'returns data as a batch' do
101  results = batch_sync(max_queries: 1) do
102    [{ id: 1 }, { id: 2 }].map { |args| resolve(args) }
103  end
104
105  expect(results).to eq(expected_results)
106end
107
108def resolve(args = {}, context = { current_user: current_user })
109  resolve(described_class, obj: obj, args: args, ctx: context)
110end
111```
112
113We can also use [QueryRecorder](../query_recorder.md) to make sure we are performing only **one SQL query** per call.
114
115```ruby
116it 'executes only 1 SQL query' do
117  query_count = ActiveRecord::QueryRecorder.new { subject }.count
118
119  expect(query_count).to eq(1)
120end
121```
122