Managing Relationships in GraphQL
One of GraphQL's most powerful features is its ability to handle complex relationships between data. In this lesson, we'll explore different types of relationships and how to query nested data efficiently.
Types of Relationships
There are three main types of relationships in databases:
1. One-to-One Relationship
Each record in one table is related to exactly one record in another table.
// Prisma Schema
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
// GraphQL Schema
type User {
id: Int!
email: String!
profile: Profile
}
type Profile {
id: Int!
bio: String!
user: User!
}
// Resolvers
const resolvers = {
User: {
profile: async (parent, args, { prisma }) => {
return await prisma.profile.findUnique({
where: { userId: parent.id }
});
}
},
Profile: {
user: async (parent, args, { prisma }) => {
return await prisma.user.findUnique({
where: { id: parent.userId }
});
}
}
};
2. One-to-Many Relationship
Each record in one table can be related to multiple records in another table.
// Prisma Schema
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
// GraphQL Schema
type User {
id: Int!
email: String!
posts: [Post!]!
}
type Post {
id: Int!
title: String!
author: User!
}
// Resolvers
const resolvers = {
User: {
posts: async (parent, args, { prisma }) => {
return await prisma.post.findMany({
where: { authorId: parent.id }
});
}
},
Post: {
author: async (parent, args, { prisma }) => {
return await prisma.user.findUnique({
where: { id: parent.authorId }
});
}
}
};
3. Many-to-Many Relationship
Multiple records in one table can be related to multiple records in another table.
// Prisma Schema
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
// GraphQL Schema
type Post {
id: Int!
title: String!
categories: [Category!]!
}
type Category {
id: Int!
name: String!
posts: [Post!]!
}
// Resolvers
const resolvers = {
Post: {
categories: async (parent, args, { prisma }) => {
const post = await prisma.post.findUnique({
where: { id: parent.id },
include: { categories: true }
});
return post.categories;
}
},
Category: {
posts: async (parent, args, { prisma }) => {
const category = await prisma.category.findUnique({
where: { id: parent.id },
include: { posts: true }
});
return category.posts;
}
}
};
Prisma handles many-to-many relationships automatically by creating a join table behind the scenes. You don't need to manually define the intermediate table.
Nested Resolvers
GraphQL allows clients to query nested relationships in a single request:
query {
user(id: 1) {
id
email
posts {
id
title
categories {
id
name
}
}
}
}
This query is resolved through a chain of resolvers:
Query.user resolver fetches the user
User.posts resolver fetches the user's posts
Post.categories resolver fetches categories for each post
Performance Warning: Nested resolvers can lead to the N+1 problem, where you make N additional queries for N related items. This can severely impact performance.
The N+1 Problem
Consider this query that fetches 10 posts and their authors:
query {
posts {
id
title
author {
name
}
}
}
Without optimization, this executes:
- 1 query to fetch posts
- 10 queries to fetch each post's author (N+1 problem)
Solving N+1 with DataLoader
DataLoader batches and caches database requests within a single request cycle:
npm install dataloader
const DataLoader = require('dataloader');
// Create a loader that batches user lookups
const createUserLoader = (prisma) => {
return new DataLoader(async (userIds) => {
// Batch fetch all users in one query
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});
// Return users in the same order as requested IDs
return userIds.map(id =>
users.find(user => user.id === id)
);
});
};
// Add to context
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
return {
prisma,
loaders: {
user: createUserLoader(prisma)
}
};
}
});
// Use in resolver
const resolvers = {
Post: {
author: async (parent, args, { loaders }) => {
// This will be batched with other author lookups
return await loaders.user.load(parent.authorId);
}
}
};
DataLoader automatically batches multiple load() calls that happen in the same tick of the event loop, reducing database queries from N+1 to just 2.
Efficient Nested Queries with Prisma
Prisma's include option allows you to fetch nested data in a single query:
const resolvers = {
Query: {
posts: async (parent, args, { prisma }) => {
return await prisma.post.findMany({
include: {
author: true, // Fetch author in same query
categories: true // Fetch categories in same query
}
});
}
},
Post: {
// No need for separate resolvers - data is already loaded
author: (parent) => parent.author,
categories: (parent) => parent.categories
}
};
Pagination for Large Relationships
When dealing with large lists, always paginate:
type User {
id: Int!
email: String!
posts(skip: Int, take: Int): [Post!]!
}
type Query {
user(id: Int!): User
}
const resolvers = {
User: {
posts: async (parent, { skip = 0, take = 10 }, { prisma }) => {
return await prisma.post.findMany({
where: { authorId: parent.id },
skip,
take,
orderBy: { createdAt: 'desc' }
});
}
}
};
query {
user(id: 1) {
email
posts(skip: 0, take: 5) {
title
}
}
}
Cursor-Based Pagination (Relay Style)
For infinite scrolling, cursor-based pagination is more efficient:
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type User {
posts(first: Int!, after: String): PostConnection!
}
const resolvers = {
User: {
posts: async (parent, { first, after }, { prisma }) => {
const posts = await prisma.post.findMany({
where: { authorId: parent.id },
take: first + 1, // Fetch one extra to check if there's more
...(after && { cursor: { id: parseInt(after) }, skip: 1 }),
orderBy: { id: 'asc' }
});
const hasNextPage = posts.length > first;
const edges = posts.slice(0, first).map(post => ({
node: post,
cursor: post.id.toString()
}));
return {
edges,
pageInfo: {
hasNextPage,
endCursor: edges[edges.length - 1]?.cursor
}
};
}
}
};
Practice Exercise:
- Create a Prisma schema with User, Post, and Comment models (User has many Posts, Post has many Comments)
- Implement GraphQL resolvers for all nested relationships
- Create a query that fetches a user with their posts and each post's comments
- Implement DataLoader to solve the N+1 problem for comment authors
- Add pagination to the User.posts field
- Test with GraphQL Playground and verify the number of database queries