admin管理员组文章数量:1422323
I'm wondering if there's any consensus out there with regard to how best to handle GraphQL field arguments when using Dataloader. The batchFn
batch function that Dataloader needs expects to receive Array<key>
and returns an Array<Promise>
, and usually one would just call load( parent.id )
where parent
is the first parameter of the resolver for a given field. In most cases, this is fine, but what if you need to provide arguments to a nested field?
For example, say I have a SQL database with tables for Users
, Books
, and a relationship table called BooksRead
that represent a 1:many relationship between Users:Books.
I might run the following query to see, for all users, what books they have read:
query {
users {
id
first_name
books_read {
title
author {
name
}
year_published
}
}
}
Let's say that there's a BooksReadLoader
available within the context
, such that the resolver for books_read
might look like this:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
return await context.loaders.booksRead.load( user.id );
}
};
The batch load function for the BooksReadLoader
would make an async
call to a data access layer method, which would run some SQL like:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?);
We would create some Book
instances from the resulting rows, group by user_id
, then return keys.map(fn)
to make sure we assign the right books to each user_id
key in the loader's cache.
Now suppose I add an argument to books_read
, asking for all the books a user has read that were published before 1950:
query {
users {
id
first_name
books_read(published_before: 1950) {
title
author {
name
}
year_published
}
}
}
In theory, we could run the same SQL statement, and handle the argument in the resolver:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
const books_read = await context.loaders.booksRead.load( user.id );
return books_read.filter( function ( book ) {
return book.year_published < args.published_before;
});
}
};
But, this isn't ideal, because we're still fetching a potentially huge number of rows from the Books
table, when maybe only a handful of rows actually satisfy the argument. Much better to execute this SQL statement instead:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?) AND B.year_published < ?;
My question is, does the cacheKeyFn
option available via new DataLoader( batchFn[, options] )
allow the field's argument to be passed down to construct a dynamic SQL statement in the data access layer? I've reviewed but I'm still unclear if cacheKeyFn
is the way to go. I'm using apollo-server-express
. There is this other SO question: Passing down arguments using Facebook's DataLoader but it has no answers and I'm having a hard time finding other sources that get into this.
Thanks!
I'm wondering if there's any consensus out there with regard to how best to handle GraphQL field arguments when using Dataloader. The batchFn
batch function that Dataloader needs expects to receive Array<key>
and returns an Array<Promise>
, and usually one would just call load( parent.id )
where parent
is the first parameter of the resolver for a given field. In most cases, this is fine, but what if you need to provide arguments to a nested field?
For example, say I have a SQL database with tables for Users
, Books
, and a relationship table called BooksRead
that represent a 1:many relationship between Users:Books.
I might run the following query to see, for all users, what books they have read:
query {
users {
id
first_name
books_read {
title
author {
name
}
year_published
}
}
}
Let's say that there's a BooksReadLoader
available within the context
, such that the resolver for books_read
might look like this:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
return await context.loaders.booksRead.load( user.id );
}
};
The batch load function for the BooksReadLoader
would make an async
call to a data access layer method, which would run some SQL like:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?);
We would create some Book
instances from the resulting rows, group by user_id
, then return keys.map(fn)
to make sure we assign the right books to each user_id
key in the loader's cache.
Now suppose I add an argument to books_read
, asking for all the books a user has read that were published before 1950:
query {
users {
id
first_name
books_read(published_before: 1950) {
title
author {
name
}
year_published
}
}
}
In theory, we could run the same SQL statement, and handle the argument in the resolver:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
const books_read = await context.loaders.booksRead.load( user.id );
return books_read.filter( function ( book ) {
return book.year_published < args.published_before;
});
}
};
But, this isn't ideal, because we're still fetching a potentially huge number of rows from the Books
table, when maybe only a handful of rows actually satisfy the argument. Much better to execute this SQL statement instead:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?) AND B.year_published < ?;
My question is, does the cacheKeyFn
option available via new DataLoader( batchFn[, options] )
allow the field's argument to be passed down to construct a dynamic SQL statement in the data access layer? I've reviewed https://github./graphql/dataloader/issues/75 but I'm still unclear if cacheKeyFn
is the way to go. I'm using apollo-server-express
. There is this other SO question: Passing down arguments using Facebook's DataLoader but it has no answers and I'm having a hard time finding other sources that get into this.
Thanks!
Share Improve this question asked May 23, 2019 at 0:15 diekunstderfugediekunstderfuge 5411 gold badge7 silver badges16 bronze badges 3-
As an aside, do you really need dataloader in this context? Unless the client is actually requesting
books_read
for the same user more than once in the same request, there is no benefit to implementing dataloader for that field. – Daniel Rearden Commented May 23, 2019 at 1:11 -
Hi @DanielRearden How do you mean? In my query example I'm assuming that the response will be an array (
[User]
), not a singleUser
. Apologies if I wasn't clear on that in my question. Since the query is for many users, I would assume I want dataloader to collect alluser_id
s so I can send them to a SQL statement likeSELECT id, first_name FROM User WHERE id IN(?);
Sincebooks_read
is a field on each individual user, and theparent
for the resolver is a singleUser
, wouldn't I also want dataloader to batch thoseuser_id
s? – diekunstderfuge Commented May 23, 2019 at 12:38 - Let's continue this conversation in chat – Daniel Rearden Commented May 23, 2019 at 14:19
1 Answer
Reset to default 5Pass the id and params as a single object to the load function, something like this:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
return context.loaders.booksRead.load({id: user.id, ...args});
}
};
Then let the batch load function figure out how to satisfy it in an optimal way.
You'll also want to do some memoisation for the construction of the object, because otherwise dataloader's caching won't work properly (I think it works based on identity rather than deep equality).
本文标签: javascripthandling GraphQL field arguments using DataloaderStack Overflow
版权声明:本文标题:javascript - handling GraphQL field arguments using Dataloader? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1745363800a2655416.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论