Skip to content
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

Add example of using DataLoader #41

Open
benmccann opened this issue May 1, 2020 · 9 comments
Open

Add example of using DataLoader #41

benmccann opened this issue May 1, 2020 · 9 comments
Assignees
Labels
type: enhancement New feature or request

Comments

@benmccann
Copy link
Contributor

Thank you so much for providing this project! I'm trying to set my project up following the todo-java-tools example. I noticed that example and none of the others demonstrate using a DataLoader and I'm having a hard time figuring it out. (I do see some support was added). I'd love it if this were something that could be added to the example

Thanks again!

@marceloverdijk
Copy link
Collaborator

marceloverdijk commented May 1, 2020

Hi @benmccann thx for reaching out!

The todo-java-tools is not suited for a DataLoader as it is not retrieving any other data.

The DataLoader pattern is suited if for example a ToDo would have a Owner (Person) which would need to be queried from a database or retrieved from another webservice.

It should be setup like:

@Factory
public class DataLoaderRegistryFactory {

    private static final Logger LOG = LoggerFactory.getLogger(DataLoaderRegistryFactory.class);

    private final PersonBatchLoader personBatchLoader;

    public DataLoaderRegistryFactory(PersonBatchLoader personBatchLoader) {
        this.personBatchLoader = personBatchLoader;
    }

    @RequestScope
    public DataLoaderRegistry dataLoaderRegistry() {
        var dataLoaderRegistry = new DataLoaderRegistry();
        dataLoaderRegistry.register("person", DataLoader.newDataLoader(personBatchLoader));
        LOG.trace("Created new data loader registry");
        return dataLoaderRegistry;
    }
}

Important is to have it @RequestScope'd so for every request the DataLoaders are re-created.

@Singleton
public class PersonBatchLoader implements BatchLoader<Long, Person> {

    private static final Logger LOG = LoggerFactory.getLogger(PersonBatchLoader.class);

    private final PersonRepository personRepository;
    private final ExecutorService executor;

    public PersonBatchLoader(PersonRepository personRepository,
            @Named(TaskExecutors.IO) ExecutorService executor) {
        this.personRepository = personRepository;
        this.executor = executor;
    }

    @Override
    public CompletionStage<List<Person>> load(List<Long> keys) {
        LOG.debug("Batch loading persons with keys: {}", keys);
        return CompletableFuture.supplyAsync(() -> keys
                .stream()
                .map(id -> personRepository.findById(id).orElse(null))
                .collect(Collectors.toList()), executor);
    }
}

and then in a DataFetcher:

@Singleton
public class ToDoOwnerDataFetcher implements DataFetcher<CompletionStage<Person>> {

    private static final Logger LOG = LoggerFactory.getLogger(ToDoOwnerDataFetcher.class);

    @Override
    public CompletionStage<Person> get(DataFetchingEnvironment environment) {
        ToDo toDo = environment.getSource();
        var ownerId = toDo.getOwnerId();
        if (ownerId == null) {
            return null;
        }
        LOG.debug("Fetching owner with id: {}", ownerId);
        DataLoader<Long, Person> personDataLoader = environment.getDataLoader("person");
        return personDataLoader.load(ownerId);
    }
}

But this is for a plain Java implementation, not using Java Tools.
I'm not familiar myself with Java Tools so I don't know if and how they support the DataLoader pattern.

Hope this helps.

@benmccann
Copy link
Contributor Author

Nice! This is super helpful!!

Is the personDataLoader something that is automatically provided by micronaut-graphql?

Will the GraphQL execution engine sometimes call the data loader (e.g. when a join is occuring) or do I always need to manually invoke it from a DataFetcher?

I don't think Java Tools does anything special as far as data loaders go, so adding this to chat or any of the other examples would be just as equally helpful

Thanks again for all your help on this!

@marceloverdijk marceloverdijk self-assigned this May 3, 2020
@marceloverdijk
Copy link
Collaborator

Hi @benmccann

I will update the ToDo examples to include a DataLoader example this week! Stay tuned.

@benmccann
Copy link
Contributor Author

That would be fantastic! Thank you so much!!

@benmccann
Copy link
Contributor Author

Hey @marceloverdijk I just thought I'd check in and see if you might still have a chance to update the example? Thanks again for all your help

@marceloverdijk
Copy link
Collaborator

Sorry @benmccann got delayed a bit but thx for the reminder. Will look at it soon!

@benmccann
Copy link
Contributor Author

I figured out how to do this, so feel free to close

I do have one Micronaut-specific question though. There are a couple places in the setup where I need to inject everything of a certain type. E.g. inject an instance of every BatchLoader to register in DataLoaderRegistryFactory or inject an instance of every GraphQLQueryResolver to register in GraphQLFactory. This can be a little tedious and verbose. Is there a way to inject a List of every subclass of a certain type?

I saw someone mention they were doing this in Spring with:

@Autowired
public RegistryBuilder(List<BatchLoader> batchLoaders)

@marceloverdijk
Copy link
Collaborator

No pls keep it open. I just didn't find the time yet :-(

The pattern you are talking about I'm also seeing with DataFetcherss and indeed it is a bit tedious and verbose.
I actually don't know if Micronaut support List injections of a certain type - like Spring - but I assume so. I used that technique in the past in a Spring application as well, but honestly I don't kow which pattern I prefer :-)

@benmccann
Copy link
Contributor Author

It turns out you can inject a List of all instances. I simplified my code below. Feel free to steal.

@Factory
@Slf4j
public class GraphQLFactory {

  @Singleton
  public GraphQL graphQL(List<GraphQLResolver<?>> resolvers) {

    log.debug("Registering " + resolvers.size() + " GraphQLResolvers");

    SchemaParserBuilder builder =
        SchemaParser.newParser().file("schema.graphql").resolvers(resolvers);

    // Create the executable schema.
    GraphQLSchema graphQLSchema = builder.build().makeExecutableSchema();

    // Return the GraphQL bean.
    return GraphQL.newGraphQL(graphQLSchema).build();
  }
}
@Factory
public class DataLoaderRegistryFactory {

  private final List<BatchLoader<?, ?>> batchLoaders;

  @Inject
  public DataLoaderRegistryFactory(List<BatchLoader<?, ?>> batchLoaders) {
    this.batchLoaders = batchLoaders;
  }

  /**
   * It seems a little inefficient to create every DataLoader for every request, but that's what the
   * API is. https://github.com/graphql-java/graphql-java/issues/1902
   */
  @RequestScope
  public DataLoaderRegistry dataLoaderRegistry() {
    DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry();
    for (BatchLoader<?, ?> loader : batchLoaders) {
      dataLoaderRegistry.register(
          loader.getClass().getSimpleName(), DataLoader.newDataLoader(loader));
    }
    return dataLoaderRegistry;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants