Is there a way to implement graphql cursor based pagination with nested pagination queries in a performant way?
Let's say we have 3 pseudo graphql types:
type User {
id: ID!
books: [Book!]!
}
type Book {
isbn: ID!
pages: [Page!]!
}
type Page {
num: ID!
}
For simplicity let's say user could read thousands of books and each book can have hundred of pages.
user
database table:
id
1
2
3
...etc
book
database table:
isbn
1
2
3
...etc
page
database table:
num
1
2
3
...etc
user_book
database table:
user_id | book_isbn
1 | 2
1 | 3
2 | 1
...etc
book_page
database table:
book_isbn | page_num
1 | 1
1 | 2
2 | 3
...etc
We can't load million users, thousands of books they read and hundreds of pages, so we do a pagination. Let's say we want to load 100 users, first 50 books that each of them read and first 10 pages of each book:
{
users(first: 100, after: "9") {
edges {
node {
id
books(first: 50) {
edges {
node {
id
pages(first: 10) {
node {
id
}
}
}
}
}
}
}
}
We can load users 10..110
, then for each user books
resolver use parent user id
to load 50 books and for each book pages
resolver load 10 pages:
// pseudo code
const resolvers = {
// get users from 10 to 110
users: (_, args) => `SELECT * FROM user WHERE id > args.after LIMIT 100`,
User: {
books: (root) => `SELECT * FROM book JOIN user_book ub WHERE ub.user_id = root.id LIMIT 50`
},
Book: {
pages: (root) => `SELECT * FROM page JOIN book_page bp WHERE bp.book_isbn = root.isbn LIMIT 10`
}
};
Problem 1 SQL
We do 1 database request to get 100 users, then 100 requests to get books for each user and finally 500 requests to get pages for each book (100 users * 50 books). Total of 601 database request for one query :(
If we didn't have pagination we could use dataloader to batch user ids into array in books resolver and book ids in pages resolver to do only 3 database requests. But how can having array of user ids 10...100
can we query only 50 books for each user and the same for pages?
Problem 2 Graphql
We can't use cursor for nested paginated queries (books and pages) because we don't know them and even if we did we could not pass them separately for each group:
{
users(first: 100, after: "9") {
edges {
node {
id
// this will not work because for each user books group we need it's own cursor
books(first: 50, after: "99") {
edges {
node {
id
}
}
}
}
}
}
My only idea to solve this is to allow pagination only as top level query and never use it as field on type.