howto resolve fields from multiple datasources in GraphQL

Viewed 1266

A beginner question about setting up GraphQL. I'm struggling to find the right way to setup resolvers for my schema the most efficient way. Say in my schema I have a User type. Some field are resolved in one backend api, others in another.

type User {
   name: String
   address: String
   dateOfBirth: String
   department: String
   function: String
   manager: String
}

name, address and dateOfBirth are from a basic administration, the other fields are from an organisation database. Suppose this is my resolver:

Query {
  User(parent, args, ctx) {
    return {
       name: '....',
       address: '...',
       dateOfBirth: '.....'
    }
  }
} 

And resolvers for the specific subfields:

User {
   department(parent, args, ctx) {
   }
   function(parent, args, ctx) {
   }
   manager(parent, args, ctx) {
   }
}

This would result in 4 requests if the user requested all fields. The last three request could have been 1 request to get all fields in one go. I would of course like that to be 2 requests: one for the base information and one for the organisation API. That would perhaps lead to this schema:

type User {
   name: String
   address: String
   dateOfBirth: String
   organisation: Organisation
} 
type Organisation {
   department: String
   function: String
   manager: String
}

and my resolvers for the subfield:

User {
   organisation {
      return {
         department: '...',
         function: '...'
         manager: '...'
      }
   }
} 

Now the request to the Organisation API gets requested only once. However, the schema looks weird: those fields should be part of a sub-object. And if we e.g. were to move the manager data to it's own API, the schema would break if we move it out of the sub-object Organisation. I tried solving this with a dataloader, but after trying some code examples, I think dataloader is more about the n+1 problem, revolving around keys of the same object type, than batching different fields. So, what would be the right way to go about this?

2 Answers

Whatever is resolved (returned) on the parent resolver is propagated to the child resolvers. So in User you could just do:

Query {
  User(parent, args, ctx) {
    //Call your API here that returns the organization fields
    const organization = getOrganizationFields();
    return {
       ...organization
       name: '....',
       address: '...',
       dateOfBirth: '.....'
    }
  }
} 

Then on the child resolvers (field resolvers) that information would be available, so you can do:

User {
   department(parent, args, ctx) {
     return parent.department
   }
   manager(parent, args, ctx) {
     return parent.manager
   }
}

Some implementations such as graphql-js make these "unitary" resolvers implicit so you don't have to write them. Of course, if you want to batch/cache the API requests you should use a DataLoader

That would perhaps lead to this schema:

type User {
   name: String
   address: String
   dateOfBirth: String
   organisation: Organisation
} 
type Organisation {
   department: String
   function: String
   manager: String
}

and my resolvers for the subfield:

User {
   organisation {

      // fetch from organisation service using parent.id
      
      return {
         department: '...',
         function: '...'
         manager: '...'
      }
   }
} 

Now the request to the Organisation API gets requested only once.

You're on a good path in the right direction as in general graphql lets you request only what you need.

Albert's [user level resolver] overfetching could be right with one datasource (one SQL request with joins).

However, the schema looks weird: those fields should be part of a sub-object.

Sure, but it doesn't mean it can't be resolved this way.

And if we e.g. were to move the manager data to it's own API, the schema would break if we move it out of the sub-object Organisation.

yes ... probably you should have even more structural schema

... you're probably:

  • looking at this schema using existing service/BE perspective;
  • mixing breaking changes/API versioning problem into this context;

... but you can already define 'the right schema', f.e.

type User {
  name: String
  address: String
  dateOfBirth: String
  role: Role
} 
type Role {
  organisation: Organisation
  function: String
}
type Organisation {
  department: String
  manager: User

}

... assuming only one role per user ... use array otherwise

In this case role and organisation subobject can be resolved from one call to organisation service - yust

return {
  function: "team lead",
  organisation: {
    id: "someDeptID",
    department: "some",
  }
}

... if you query will require manager field then it will (should) be resolved by Organisation.manager resolver. Of course you can return it there (in role resolver) if data is already fetched/available.

Related