How to use DataLoader for Connections with Hot Chocolate GraphQL

Viewed 2625

I see that it's possible to use Data Loaders for root queries, but is it also possible to use Data Loaders for nested connections? In the example below, I want use a Data Loader for the rooms property. In the example request at the bottom, there will be three database queries made. One by the data loader to fetch both buildings, one to fetch the rooms for building 1, and another to fetch the rooms for building 2. Instead, I'm trying to use a data loader for the rooms, so only two database queries are made.

// Building DB table
ID | Name
1  | Main Campus
2  | Satellite Campus
// Rooms DB table
ID | BuildingId | Name
1  | 1          | Lab
2  | 1          | Dorm
3  | 2          | Theatre
4  | 2          | Gym
// Schema
type Building {
  id: Int!
  name: String!
  rooms(after: String before: String first: PaginationAmount last: PaginationAmount): RoomsConnection
}

type Room {
  id: Int!
  name: String!
  building: Building!
}
// Hot Chocolate
public class BuildingType: ObjectType<Building> {
  protected override void Configure(IObjectTypeDescriptor<Building> descriptor)
  {
    // ... omitted other fields for brevity

    // Instead of using a resolver, can a data loader be used instead?
    descriptor.Field(b => b.rooms).UsePaging<RoomType>().Resolver(ctx => {
      var building = ctx.Parent<Building>();
      var roomsRepository = ctx.Service<IRoomsRepository>();
      return roomsRepository.GetRoomsByBuildingId(building.Id);
    });
  }
}
// Example request
query {
  a: building(id: 1){
    id,
    name,
    rooms {
      nodes {
        id,
        name
      }
    }
  },
  b: building(id: 2){
    id,
    name,
    rooms {
      nodes {
        id,
        name
      }
    }
  }
}
1 Answers

Not only is that possible but I would say it's the basic use case for DataLoaders to avoid "N+1" requests made across the object tree. For your purpose you could use GroupDataLoader. The group data loader is a batch data loader, i.e. it collects requests for similar entities made in one graphql request roundtrip and sends them as a single request to the data source. After that, it caches the result - i.e. returning entities from the cache while requested from any place of the object tree. Group data loader gets the list of keys as an input parameter (IReadOnlyList<TKey>) and returns the list of cached entities as a lookup (ILookup<TKey, TValue>). Lookup is a data type which holds multiple values per key (rather than dictionary which holds a single value per one key).

According to your specific case, rooms have to be grouped by building id - it is the key. So, you could use the group data loader to cache your connection in the following way (take into account that it's worthwhile to change the interface of your RoomsRepository to support batch requests to it, i.e. it has to accept not a single building id but a batch of them, also I assume that Room contains back reference to its building id):

   public class BuildingType : ObjectType<Building>
{
    protected override void Configure(IObjectTypeDescriptor<Building> descriptor)
    {
        descriptor.Field(b => h.rooms).UsePaging<RoomType>
.Resolver(async (ctx, t) => await ctx.GroupDataLoader<int, Room>("roomsByBuildingGroup",
                    async keys => ctx.Service<IRoomsRepository>().GetRoomsByBuildingIds(keys).ToLookup(r => r.BuildingId))
                .LoadAsync(ctx.Parent<Building>().Id, t));

}

"roomsByBuildingGroup" is the name of the loader. In order to share a cache and to combine all requests from different objects tree places into a single one you have to use the same loader (i.e. use the same loader name) in all other places where you load rooms by building id.

Related