GraphQL

Relationships and Nested Data

18 min Lesson 9 of 35

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:

  1. Query.user resolver fetches the user
  2. User.posts resolver fetches the user's posts
  3. 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:
  1. Create a Prisma schema with User, Post, and Comment models (User has many Posts, Post has many Comments)
  2. Implement GraphQL resolvers for all nested relationships
  3. Create a query that fetches a user with their posts and each post's comments
  4. Implement DataLoader to solve the N+1 problem for comment authors
  5. Add pagination to the User.posts field
  6. Test with GraphQL Playground and verify the number of database queries