#Relationship

Relationship are integral part of GraphQL. It define how one entity integrate with another.

#Relationship Resolver

Relationship field (fields that are referencing other types) can be done using a relationship resolver, which is similar to any field resolver.

Say we have a type Car that have many Parts, where each Part holds the id for the Car it is for.

import struct Pioneer.ID struct Car: Identifiable { var id: ID var model: String } struct Part: Identifiable { var id: ID var name: String var carId: Car.ID }
1
2
3
4
5
6
7
8
9
10
11
12

#Resolver on Object type

Using extensions, we can describe a custom resolver function to fetch the Car for a given Part, and getting all the Part for a given Car.

extension Car { func parts(ctx: Context, _: NoArguments) async throws -> [Part] { try await ctx.db.find(Part.self).filter { $0.carId == id } } } extension Part { func car(ctx: Context, _: NoArguments) async throws -> User? { try await ctx.db.find(Car.self).first { $0.id == carId } } }
1
2
3
4
5
6
7
8
9
10
11

And update the schema accordingly.

type Car { id: ID! model: String! parts: [Part!]! } type Part { id: ID! name: String! car: Car }
1
2
3
4
5
6
7
8
9
10
11
Schema in Graphiti
import Graphiti import Pioneer func schema() throws -> Schema<Resolver, Context> { try .init { ID.asScalar() Type(Car.self) { Field("id", at: \.id) Field("model", at: \.model) Field("parts", at: Car.parts, at: [Part].self) } Type(Part.self) { Field("id", at: \.id) Field("name", at: \.name) Field("car", at: Part.car, as: Car?.self) } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#N+1 Problem

Imagine your graph has query that lists items

query { parts { name car { id model } } }

with the parts resolver looked like

Resolver.swift
struct Resolver { func parts(ctx: Context, _: NoArguments) async throws -> [Part] { try await ctx.db.find(Part.self) } }

The graph will executed that Resolver.parts function which will make a request to the database to get all items.

Let's assume the database is a SQL database and the following SQL statements created when resolving the query are:

SELECT * FROM parts SELECT * FROM cars WHERE id = ? SELECT * FROM cars WHERE id = ? SELECT * FROM cars WHERE id = ? SELECT * FROM cars WHERE id = ? SELECT * FROM cars WHERE id = ? ...
1
2
3
4
5
6
7

What's worse is that certain parts can be for the same car so these statements will likely query for the same users multiple times.

This is what's called the N+1 problem which you want to avoid. The solution? DataLoader.

#DataLoader

The GraphQL Foundation provided a specification for solution to the N+1 problem called dataloader. Essentially, dataloaders combine the fetching of process across all resolvers for a given GraphQL request into a single query.

The package Dataloader implement that solution for GraphQLSwift/GraphQL.

.package(url: "https://github.com/GraphQLSwift/DataLoader", from: "...")
1

After that, we can create a function to build a new dataloader for each operation, and update the relationship resolver to use the loader

struct Context { var db: Database var carLoader: DataLoader<Car.ID, Car?> var partsLoader: DataLoader<Car.ID, [Part]> } extension Car { func loader(ev: EventLoop, db: Database) -> DataLoader<Car.ID, Car?> { .init(on: ev) { keys in let cars = try? await db.find(Car.self).filter { keys.contains($0.id) } return keys.map { key in guard let car = cars?.first(where: { $0.id == key }) else { return .succes(nil) } return .success(car) } } } } extension Part { func loader(ev: EventLoop, db: Database) -> DataLoader<Car.ID, [Part]> { .init(on: ev) { keys in let all = try? await db.find(Part.self).filter { keys.contains($0.carId) } return keys.map { key in guard let parts = all?.filter({ $0.carId == key }) else { return .success([]) } return .success(parts) } } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

#Using dataloader in resolvers

extension Car { func parts(ctx: Context, _: NoArguments) async throws -> [Part] { try await ctx.partsLoader.load(key: id, on: ev) } } extension Part { func car(ctx: Context, _: NoArguments, ev: EventLoopGroup) async throws -> User? { try await ctx.carLoader.load(key: carId, on: ev) } }
1
2
3
4
5
6
7
8
9
10
11

Now instead of having n+1 queries to the database by using the dataloader, the only SQL queries sent to the database are:

SELECT * FROM parts SELECT * FROM cars WHERE id IN (?, ?, ?, ?, ?, ...)

which is significantly better.