0
votes

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
On the resolver of User, which has to return an object of type User, simply make the two API calls there and compose the object which is returned. This User object is then propagated to the child resolvers, which will be already resolved, so you just have to get the field from the parent resolver and return it (some specifications do it by default so you could just omit the resolver on each field)Albert Alises
@AlbertAlises - the problem is when the different field dept, function and manager, you want those to be a sep. api call, only to be executed when the client asks for them. that would suggest putting them in a seperate subfield so they are grouped, or having resolvers for all fields sperately. In the first case you would be modelling according to the way the fields are resolved, which I thing is not good practice. The second option would be prefferable, but than every field requested would result in a separate api call. So I need some batching of requested fields, resulting in 1 api call.micksp

2 Answers

0
votes

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

0
votes

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.