-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cleaner and simpler field deferring #738
Comments
Interesting @oprypkhantc, can you explain a bit more on the use case for this? Where is this needed where prefetch isn't an ideal solution? And how does deferring the execution of the field affect the output (load time)? I'm under the assumption that fields are only executed if they're part of the operation's GQL. |
Sure. In our case, we're trying to optimize fields that do SQL aggregations into one SQL query, e.g.: #[Type]
class User {
#[Field]
public function commentsCount(
#[Prefetch('prefetchCommentsCount') array $prefetched,
): int {
return $prefetched[$this->id];
}
/**
* @param list<User> $users
*/
public static function prefetchCommentsCount(array $users): array
{
return Collection::wrap($users)
->loadCount([
'comments as comments_count',
])
->mapWithKeys(fn (User $user) => [
$user->id => (int) $user->comments_count,
])
->all();
}
#[Field]
public function blogsCount(
#[Prefetch('prefetchBlogsCount') array $prefetched,
): int {
return $prefetched[$this->id];
}
/**
* @param list<User> $users
*/
public static function prefetchBlogsCount(array $users): array
{
return Collection::wrap($users)
->loadCount([
'blogs as blogs_count',
])
->mapWithKeys(fn (User $user) => [
$user->id => (int) $user->blogs_count,
])
->all();
}
} Which executes two SQL requests (once in query {
users(ids: [1, 2, 3]) {
nodes {
commentsCount
blogsCount
}
}
} select id, (select count(*) from comments where user_id = users.id) as comments_count
from users
where id in (1, 2, 3);
select id, (select count(*) from blogs where user_id = users.id) as blogs_count
from users
where id in (1, 2, 3); When a model has 10+ available aggregated fields, it doesn't seem like a good idea to make 10 separate queries instead of 1. So I've looked at how other GraphQL implementations do that, and it seems that you can do it in a different way: #[Type]
class User {
/** @return int */
#[Field]
public function commentsCount(
#[Autowire] EloquentBatchLoader $loader,
ResolveKey $resolveKey,
) {
return $loader->defer(
$resolveKey,
$this,
fn (Builder $query) => $query->withCount([
'comments as comments_count',
]),
fn (User $user) => (int) $user->comments_count,
);
}
/** @return int */
#[Field]
public function blogsCount(
#[Autowire] EloquentBatchLoader $loader,
ResolveKey $resolveKey,
) {
return $loader->defer(
$resolveKey,
$this,
fn (Builder $query) => $query->withCount([
'blogs as blogs_count',
]),
fn (User $user) => (int) $user->blogs_count,
);
}
} Under the hood, select id,
(select count(*) from comments where user_id = users.id) as comments_count,
(select count(*) from blogs where user_id = users.id) as blogs_count
from users
where id in (1, 2, 3); Basically the same thing |
What is the current behavior of a callable return type? Is that even supported? You're suggesting that a callable is wrapped and treated as a deferred by default? So the proposal would be to support |
None, it throws
Yes - it sounds logical to me at least :)
Yes, at least that's the idea. Supporting |
I believe almost zero.
That's correct, but I'm not sure what you're referring to |
I thought you were doing something else, but it's clear now, so disregard. I really don't see an issue with the proposal. Being that a How would a callable with with arguments be treated? Is there a default |
I guess throw an exception if it has any arguments when mapping the type? It wouldn't be possible to call the callable if it has required parameters anyway. The only way it would work is if callable's parameters are optional, but I don't think there's a point in checking for that 🤔 |
Well, an argument could be made for a default signature. I don't know what the ideal one would be though. Of course, those would be passed arguments. The userland callable wouldn't have to implement them. It could also be possible to define the arguments within an attribute. For instance, instead of the That all said, it probably makes the most sense to stick with an exception and simple implementation until there are more clearly defined requirements. |
Hey
We've found use cases for deferring field resolution directly, without parameters or prefetch, e.g.:
Currently it's possible, but ugly:
outputType
GraphQL\Deferred
new GraphQL\Deferred
instance everywhereI propose that such cases are simplified into returning a (typed) callable, which removes the dependency on
Deferred
from userland code, as well as allows specifying an actual PHPDoc type:The implementation should be somewhat trivial - under the hood the callable value would just get wrapped in
Deferred
, and additional root type mapper would handlecallable()
PHPDoc types. Thoughts?The text was updated successfully, but these errors were encountered: