-
Notifications
You must be signed in to change notification settings - Fork 15
Manual
graphql-java is an implementation of the GraphQL specification using Java. To define a GraphQL schema, graphql-java
requires every type and field to be explicitly registered with the library.
While this provides a straight forward mechanism for simple apps (those with a few domain objects), it can quickly become rather hard to maintain - particularly in complex applications composed of dozen or hundreds of types.
GLiTR seeks to address this problem by lifting the burden of manual type registration, thus freeing the developer to focus on the API design. GLiTR can in fact register all types automatically as long as they belong to the API Graph.
To illustrate this, let's begin with the Todo MVC example found in graphql-java
and then augment it with GLiTR.
Here is the Todo
java class taken from the todomvc-relay-java example:
public class Todo {
private String id;
private String text;
private boolean complete;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public boolean isComplete() { return complete; }
public void setComplete(boolean complete) { this.complete = complete; }
}
Along with the code that registers the Todo
GraphQL type.
private void createTodoType() {
todoType = newObject()
.name("Todo")
.field(newFieldDefinition()
.name("id")
.type(new GraphQLNonNull(GraphQLID))
.dataFetcher(environment -> {
Todo todo = (Todo) environment.getSource();
return relay.toGlobalId("Todo", todo.getId());
}
)
.build())
.field(newFieldDefinition()
.name("text")
.type(Scalars.GraphQLString)
.build())
.field(newFieldDefinition()
.name("complete")
.type(Scalars.GraphQLBoolean)
.build())
.withInterface(nodeInterface)
.build();
}
That is a fair bit of boilerplate code needed for a simple Object and it follows that it'll increase the maintenance burden in kind. Here is the equivalent with GLiTR.
public class Todo {
private String id;
private String text;
private boolean complete;
public String getId() { return id; }
public String getId(DataFetchingEnvironment env) {
Todo todo = (Todo) environment.getSource();
return relay.toGlobalId("Todo", todo.getId());
}
public void setId(String id) { this.id = id; }
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public boolean isComplete() { return complete; }
public void setComplete(boolean complete) { this.complete = complete; }
}
With GLiTR, the GraphQL schema now maps onto the Java domain, eliminating the need for an additional class entirely. As a result, updating the schema is now as easy as updating a POJO, greatly reducing the maintenance overhead necessitated by graphql-java
.
Quick note: for the sake of brevity, the id
field data fetcher is declared inside our domain class (i.e. added a DataFetchingEnvironment
to the getId
method). See Override data fetcher inside the schema definition class section for more details.
To look at a full example, head to the Todo Application.
GLiTR allows defining the GraphQL Schema with POJOs. Here is a mapping table of the supported types:
GraphQL type | Java type |
---|---|
GraphQLString | String |
GraphQLBoolean | Boolean |
GraphQLInt | Integer |
GraphQLFloat | Float |
GraphQLObjectType | Any POJO |
GraphQLInterfaceType | Any interface type |
GraphQLUnionType | Not supported |
GraphQLEnumType | Any enum type |
GraphQLInputObjectType | Any POJO |
GraphQLList | Any implementing class of Collection |
GraphQLNonNull | See @GlitrNonNull
|
Since all types are discovered and inspected by GLiTR, exposing a GraphQL type is as easy as creating a POJO. All public methods starting with get
are exposed.
Example:
public class User {
private String id;
private Integer age;
private String firstName;
private String lastName;
public String getId() { // `id` property is exposed
return id;
}
public void setId(String id) { this.id = id; }
public Integer getAge() { // `age` property is exposed
return age;
}
public void setAge(Integer age) { this.age = age; }
public String getFirstName() { // `firstName` property is exposed
return firstName;
}
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { // `lastName` property is exposed
return lastName;
}
public void setLastName(String lastName) { this.lastName = lastName; }
}
Note: GraphQL type names must be unique across the graph.
Same thing for interfaces.
Example:
public class User implements Person {
...
}
The Person
interface is automatically discovered and User
is registered as one of its implementing types.
public interface Person {
Integer getAge();
String getFirstName();
String getLastName();
}
Same thing for enums.
Example:
public class User {
private Gender gender;
public Gender getGender() { // `gender` property is exposed
return this.gender;
}
}
The Gender
enum type is automatically discovered.
public enum Gender {
MALE,
FEMALE;
}
Object input types are used inside GraphQL arguments. See how GLiTR exposes GraphQL Arguments for more details. A GraphQL object type can be registered as either an output or input type but not both.
Note: A POJO registered as an object-input type can not be used as an object type elsewhere. GraphQL type names must be unique.
Example:
To retrieve a user by address, with AddressInput
being a GraphQL input type.
public class Root {
private User user;
@GlitrArgument(name = "address", type = AddressInput.class, nullable = false)
public User getUserByAddress() {
return user;
}
}
The configuration doesn't fundamentally change but note the name AddressInput
is now tied to a GraphQL input type and can't be used as a GraphQL output type elsewhere.
public class AddressInput {
private String addressLine1;
private String addressLine2;
private String zipcode;
public String getAddressLine1() { return addressLine1; }
public void setAddressLine1(String addressLine1) { this.addressLine1 = addressLine1; }
public String getAddressLine2() { return addressLine2; }
public void setAddressLine2(String addressLine2) {this.addressLine2 = addressLine2; }
public String getZipcode() { return zipcode; }
public void setZipcode(String zipcode) { this.zipcode = zipcode; }
}
For lists, any implementing class of Collection is supported.
To declare a type as being non null annotate either getters or fields with @GlitrNonNull
Example:
public class User {
private String id;
@GlitrNonNull
public String getId() { // 'id' is guaranteed to be non-null when data is returned from the server.
return id;
}
public void setId(String id) { this.id = id; }
}
or
public class User {
@GlitrNonNull
private String id;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
}
GraphQL allows fields to have arguments. To add an argument, annotate either getters or fields with @GlitrArgument
.
Example:
public class Root {
private User user;
@GlitrArgument(name = "id", type = String.class, nullable = false)
public User getUserById() {
return user;
}
}
Note that @GlitrArgument
has properties matching the characteristics of GraphQL arguments.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface GlitrArgument {
String name();
Class type();
boolean nullable() default true;
String defaultValue() default "No Default Value";
String description() default "No Description";
}
@GlitrArgument
is also repeatable when used within a @GlitrArguments
container.
Example:
public class Root {
private List<User> users;
@GlitrArguments({
@GlitrArgument(name = "firstName", type = String.class, nullable = false),
@GlitrArgument(name = "lastName", type = String.class, nullable = false)
})
public List<User> getUsersByFirstNameAndLastName() {
return users;
}
}
GraphQL allows fields and types to hold a description. To add a description, annotate either class or field with @GlitrDescription
.
Example:
@GlitrDescription("Where it all begins.")
public class Root {
private User user;
@GlitrDescription("Retrieve a user by id.")
@GlitrArgument(name = "id", type = String.class, nullable = false)
public User getUserById() {
return user;
}
}
By default, GLiTR scans all getters or fields in classes that are contained in the domain graph.
If your POJOs hold properties or methods (whose names start with get
) that you do not want exposed in your API, annotate them with @GlitrIgnore
.
Example:
@GlitrDescription("A user in the system.")
public class User {
private String id;
@GlitrIgnore
private Integer age; // age property won't be exposed
private String firstName;
private String lastName;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
GLiTR not only makes the schema definition seamless, it also allows definition of data fetchers.
GLiTR scans for getters (specifically methods starting with get
), and, by default, registers a PropertyDataFetcher
for every declared field.
While this is enough for simple use cases where we want to expose static data inside a POJO, GraphQL really shines by giving the freedom to customize the data fetching logic. GLiTR enables this by making it possible to declare different data fetching strategies for any given field.
The custom strategies available are as follow:
- Override data fetcher placed outside the schema definition class
- Override data fetcher placed inside the schema definition class
- Annotation based data fetcher
- Annotation based data fetcher factory
To override the PropertyDataFetcher
behavior for a specific field, we overload the original method with an additional DataFetchingEnvironment
parameter.
Example: For instance users in our system could be accessible via an external API.
@GlitrDescription("Where it all begins.")
public class Root {
private User user;
private UserService userService; // service to fetch users
@GlitrDescription("Retrieve a user by id.")
@GlitrArgument(name = "id", type = String.class, nullable = false)
public User getUserById(DataFetchingEnvironment env) { // DataFetchingEnvironment added to trigger override
// retrieve passed argument
String userId = env.getArgument("id");
// fetch our user
return apiService.fetch(userId);
}
}
Mixing schema definition and data fetching logic can be convenient but it's generally cleaner to separate your data fetchers from your schema definition POJOs.
To override the PropertyDataFetcher
behavior for a specific field in this way we can define a method with the exact same name in a separate class and register an instance of that class to be inspected for overridden methods.
Example:
@GlitrDescription("Where it all begins.")
public class Root {
@GlitrDescription("Retrieve a user by id.")
@GlitrArgument(name = "id", type = String.class, nullable = false)
public User getUserById() { // The method names must match!
return null; // fine as the method execution will be overriden by our override
}
}
To override the data fetching of the userById
GraphQL field, we create a separate class RootOverride
that contains the same method name getUserById
and takes an additional DataFetchingEnvironment
method parameter.
public class RootOverride {
// service to fetch users
private UserService userService;
public User getUserById(DataFetchingEnvironment env) { // The method names must match!
// retrieve passed argument
String userId = env.getArgument("id");
// fetch our user
return apiService.fetch(userId);
}
}
Note that the method signature matches the one from the get
method declared inside the DataFetcher
interface.
Finally we register the override with GLiTR.
...
Glitr glitr = GlitrBuilder.newGlitr()
.withQueryRoot(new Root())
.addOverride(Root.class, new RootOverride())
.build();
Defining data fetchers for every field can become both a tedious and repetitive process so, when parts of your domain are fetched in a similar way, you can create an annotation to help you to DRY.
Example: Let say users are retrieved through a restful API. We could create a data fetcher that could handle all the fetching over a restful service.
Here is an example of such a data fetcher:
public class RestDataFetcher implements DataFetcher{
private restService;
public RestDataFetcher(String resource, String domain) {
this.domain = domain;
this.resource = resource;
}
public Object get(DataFetchingEnvironment environment) {
return restService.fetch(domain, resource, environment.getArguments());
}
}
and we can create a Rest
custom annotation to denote that annotated GraphQL fields should be resolved using the RestDataFetcher
we just created.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Rest {
String resource();
String domain();
}
We now add the annotation to the original getter.
@GlitrDescription("Where it all begins.")
public class Root {
@GlitrDescription("Retrieve a user by id.")
@GlitrArgument(name = "id", type = String.class, nullable = false)
@Rest(resource = "/users/{id}", domain = "api.nfl.com")
public User getUserById(DataFetchingEnvironment env) {
return null;
}
}
Finally we register the annotation and its data fetcher with GLiTR
...
Glitr glitr = GlitrBuilder.newGlitr()
.withQueryRoot(new Root())
.addCustomDataFetcher(Rest.class, new RestDataFetcher("/users/{id}", "api.nfl.com"))
.build();
We can now go one step further and delegate the creation of the data fetchers to a factory. This allows the extraction of valuable reflection data directly from either the annotated field, the annotated method, the declaring class or the annotation itself.
To do so, just implement the AnnotationBasedDataFetcherFactory
interface.
Example:
public class RestDataFetcherFactory implements AnnotationBasedDataFetcherFactory {
@Override
public DataFetcher create(@Nullable Field field,
@Nonnull Method method,
@Nonnull Class declaringClass,
@Nonnull Annotation annotation) {
// extract valuable data directly from the field, method, class or annotation
Rest restAnn = (Rest) annotation;
return new RestDataFetcher(restAnn.resource(), restAnn.domain()); // create a custom data fetcher on the fly
}
}
Finally we register the annotation and its factory with GLiTR
...
Glitr glitr = GlitrBuilder.newGlitr()
.withQueryRoot(new Root())
.addCustomDataFetcherFactory(Rest.class, new RestDataFetcherFactory())
.build();
If multiple data fetching strategies are registered for the same GraphQL field, they will be executed in the following order:
- Override data fetcher outside class
- Override data fetcher inside class
- Data Fetcher factory if registered, else use annotation based data fetcher
Mutation operations all hang off a root type. To expose mutations with GLiTR, register the entry point.
...
Glitr glitr = GlitrBuilder.newGlitr()
.withQueryRoot(new Root())
.withMutationRoot(new MutationType())
.build();
Similar to query operations, to expose a GraphQL field just prefix the mutation name with get
.
So to expose a createUser
mutation field we create a getCreateUser
java method.
public class MutationType {
@GlitrArgument(name = "createUserInput", type = CreateUserInput.class, nullable = false)
public User getCreateUser(DataFetchingEnvironment environment) {
Map inputMap = env.getArgument("input");
CreateUserInput input = JsonUtils.convertValue(inputMap, CreateUserInput.class);
// could validate input
// save
User user = new User("user1");
user.setFirstName(input.getFirstName());
user.setLastName(input.getLastName());
return user;
}
}
The mutation takes in a CreateUserInput
object and returns a User
object if the operation completes successfully.
public class CreateUserInput {
private String firstName;
private String lastName;
public String getFirstName() { return this.firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return this.lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
To enable Relay, just add withRelay()
when initializing GLiTR.
Example:
Glitr glitr = GlitrBuilder.newGlitr()
.withRelay() // with Relay support!
.withQueryRoot(new Viewer())
.withMutationRoot(new MutationType())
...
Relay pagination introduces the concept of a Connection
that wraps a collection into a page-information-aware object.
To expose a collection as being pageable, annotate the field or getter with @GlitrForwardPagingArguments
. GLiTR will expose two GraphQL arguments named first
and after
.
Example:
@GlitrDescription("A User Object")
public class User {
private String id;
@GlitrForwardPagingArguments
private List<Todo> todoList;
public String getId() { return id; }
public User setId(String id) {
this.id = id;
return this;
}
public List<Todo> getTodoList() { return todoList; }
public User setTodoList(List<Todo> todoList) {
this.todoList = todoList;
return this;
}
}
To handle pagination we create a custom data fetcher
public class UserOverrideDataFetcher {
public Connection getTodoList(DataFetchingEnvironment env) {
User user = (User) env.getSource();
return (Connection) new SimpleListConnection(user.getTodoList()).get(env);
}
}
and register it with GLiTR.
...
Glitr glitr = GlitrBuilder.newGlitr()
.withRelay()
.withQueryRoot(new Root())
.withMutationRoot(new Mutation())
.addOverride(User.class, new UserOverrideDataFetcher()) // we register the override for the User class
.build();
Relay exposes a Node
interface type along with a node
field for object identification.
When GLiTR is configured with Relay support every object type registered will implement the Node
interface, provided they expose an id
field or getter.
You can disable this behavior inside GLiTR configuration:
...
Glitr glitr = GlitrBuilder.newGlitr()
.withRelay()
.withQueryRoot(new Root())
.withMutationRoot(new Mutation())
.withExplicitRelayNodeScan() // classes having a `getId()` method no longer implement from `Node`
Example:
To be in compliance with the Relay spec, one must expose the node
field as a root endpoint.
public class Root {
private User user;
@GlitrDescription("Retrieve any object type by id.")
@GlitrArgument(name = "id", type = String.class, nullable = false)
public Node getNode() {
return user;
}
}
with the Node
interface
public interface Node {
Node getId();
}
and an example data fetcher.
public class RootOverride {
private nodeService;
public Node getNode(DataFetchingEnvironment environment) {
// retrieve passed argument
String nodeId = env.getArgument("id");
// fetch our object
return nodeService.fetch(nodeId);
}
}
Mutation inputs in Relay by convention should end with Input
. So it is good practice to name your input types the same way.
Relay also introduces a clientMutationId
that allows the framework to track mutations and responses. (Relay Input Object Mutations Specification)
Example: Create a user with Relay compliance.
@GlitrDescription("Root of all mutation endpoints")
public class MutationType {
@GlitrArgument(name = "input", type = CreateUserInput.class, nullable = false)
public CreateUserPayload getCreateUser(DataFetchingEnvironment env) {
Map inputMap = env.getArgument("input"); // retrieve arguments
// convert map to input type
CreateUserInput input = JsonUtils.convertValue(inputMap, CreateUserInput.class);
// save user in db
User user = new User()
.setId(input.getId())
.setTodoList(input.getTodoList());
// format response
CreateUserPayload payload = new CreateUserPayload();
payload.setClientMutationId(input.getClientMutationId());
payload.setUser(user);
return payload;
}
}
The input class
public class CreateUserInput extends RelayMutationType {
private String id;
private List<Todo> todoList;
public String getId() { return id; }
public CreateUserInput setId(String id) {
this.id = id;
return this;
}
public List<Todo> getTodoList() { return todoList; }
public CreateUserInput setTodoList(List<Todo> todoList) {
this.todoList = todoList;
return this;
}
}
and the payload class.
public class CreateUserPayload extends RelayMutationType {
private User user;
public User getUser() { return user; }
public CreateUserPayload setUser(User user) {
this.user = user;
return this;
}
}
Copyright 2016 NFL Enterprises LLC.
NFL Engineers blog | Twitter @nflengineers | Jobs