GraphQL in Microservices With Spring and Angular
This article will help software developers decide whether REST or GraphQL is best for their project(s) based on their frontend and backend implementations.
Join the DZone community and get the full member experience.
Join For FreeThe AngularAndSpringWithMaps project has been converted from REST endpoints to a GraphQL interface. The project uses Spring GraphQL to provide the backend interface. The Angular frontend uses the Angular HttpClient
to post the requests to the backend.
GraphQL vs REST From an Architectural Perspective
REST Endpoints
REST calls retrieve objects with their children. For different root objects, separate REST calls are sent. These calls can accumulate with the number of different root objects that the frontend requests. With relationships between the objects, sequential calls become necessary.
Rings for the Polygon ID’s are fetched. Then the locations for the ring ID’s are fetched, which is a real REST design. It would also be possible to create two endpoints.
- An endpoint that returns the ‘CompanySite.’
- An endpoint that returns the ‘CompanySite’ with all its children.
Then the client can request the data from the right endpoint and gets what it needs with one request.
With a growing number of requirements, the number of endpoints for clients grows. The effort to support the new endpoints will grow accordingly.
GraphQL Interface
GraphQL can support queries where the requested result object can be specified.
The client that requests a ‘CompanySite’ can specify in the query the Polygons/Rings/Locations that the server needs to provide as children to the client. That creates a very flexible interface for the frontend but needs a backend that can support all possible queries in an efficient implementation.
Considerations for a Microservice Architecture
REST endpoints and GraphQL interfaces can have different use cases.
REST Endpoints
The different REST endpoints make it easier to implement additional logic for the results. For example, calculating sums and statistics for paying users of the requested objects. The more specialized endpoints can support these features easier.
GraphQL Interfaces
The GraphQL interface is more generic in its result structure. That makes it a good fit for an implementation that collects the requested data and returns it to the client. Smarter clients that implement more of the business logic are a good fit for this architecture. They can request the data required to provide the required features.
The GraphQL server can use databases or other servers to read its data. The data reads need to be optimized for all query shapes (support for partial data requests). Due to the generic interface, the GraphQL server will have less knowledge of its clients.
GraphQL Implementation in a Microservice
The AngularAndSpringWithMaps
project uses a GraphQL interface.
GraphQL in an Angular Frontend
GraphQL clients for Angular are available, but in this use case, Angular Services with the HttpClient
are used. The GraphQLService implements the queries and mutations:
export interface GraphqlOptions {
operationName: string;
query: string;
variables?: { [key: string]: any};
}
@Injectable()
export class GraphqlService {
constructor(private http: HttpClient) { }
public query<T>(options: GraphqlOptions): Observable<T> {
return this.http
.post<{ data: T }>(`/graphql`, {
operationName: options.operationName,
query: options.query,
variables: options.variables,
})
.pipe(map((d) => d.data));
}
public mutate<T>(options: GraphqlOptions): Observable<any> {
return this.http
.post<{ data: T }>(`/graphql`, {
operationName: options.operationName,
query: options.query,
variables: options.variables,
})
.pipe(map((d) => d.data));
}
}
The GraphQLOptions
interface defines the parameters for the ‘query’ and ‘mutate’ methods of the GraphQLService
.
The GraphQLService is provided by the MapsModule and implements the ‘query’ and ‘mutate’ methods. Both methods post the content of the GraphQLOptions
to the /graphql
path. The posted object contains the properties ‘operationName,’ ‘query,’ and ‘variables.’ The returned result uses an RxJS pipe to unwrap the data in the result.
The CompanySiteService uses the GraphQLService
to request the ‘CompanySites’ by ‘name’ and ‘year:’
@Injectable()
export class CompanySiteService {
...
public findByTitleAndYear(title: string, year: number):
Observable<CompanySite[]> {
const options = { operationName: 'getCompanySiteByTitle',
query: 'query getCompanySiteByTitle($title: String!, $year:
Long!) { getCompanySiteByTitle(title: $title, year: $year) {
id, title, atDate }}', variables: { 'title': title, 'year':
year } } as GraphqlOptions;
return this.mapResult<CompanySite[],CompanySite[]>
(this.graphqlService.query<CompanySite[]>(options),
options.operationName);
}
public findByTitleAndYearWithChildren(title: string, year: number):
Observable<CompanySite[]> {
const options = { operationName: 'getCompanySiteByTitle', query:
'query getCompanySiteByTitle($title: String!, $year: Long!) {
getCompanySiteByTitle(title: $title, year: $year) { id, title,
atDate, polygons { id, fillColor, borderColor, title, longitude,
latitude,rings{ id, primaryRing,locations { id, longitude,
latitude}}}}}', variables: { 'title': title, 'year': year } } as
GraphqlOptions;
return this.mapResult<CompanySite[],CompanySite[]>
(this.graphqlService.query<CompanySite[]>(options),
options.operationName);
}
...
The findByTitleAndYear(...)
method creates the GraphQLOptions
with a query that requests ‘id, title, atDate’ of the ‘CompanySite.’
The findByTitleAndYearWithChildren(..)
method creates the GraphQLOptions
with a query that requests the ‘CompanySites’ with all its children and their properties and children.
The ‘operationName’ is used in the mapResult(...)
method to unwrap the result.
Conclusion Frontend
In this use case, Angular provided the needed tools, and GraphQL is easy enough to use with Angular only. To develop the GraphQL queries for the backend, GraphQL (/graphiql)
can help.
GraphQL in the Spring Boot Backend
Spring Boot provides good support for GraphQL backend implementations. Due to the many options in the queries, the implementation has to be much more flexible than a REST implementation.
GraphQL Schema
The schema is in the schema.graphqls file:
scalar Date
scalar BigDecimal
scalar Long
input CompanySiteIn {
id: ID!
title: String!
atDate: Date!
polygons: [PolygonIn]
}
type CompanySiteOut {
id: ID!
title: String!
atDate: Date!
polygons: [PolygonOut]
}
...
type Query {
getMainConfiguration: MainConfigurationOut
getCompanySiteByTitle(title: String!, year: Long!): [CompanySiteOut]
getCompanySiteById(id: ID!): CompanySiteOut
}
type Mutation {
upsertCompanySite(companySite: CompanySiteIn!): CompanySiteOut
resetDb: Boolean
deletePolygon(companySiteId: ID!, polygonId: ID!): Boolean
}
The ‘scalar Date’ imports the ‘Date’ datatype from the graphql-java-extended-scalars
library for use in the schema file. The same is implemented for the data types ‘BigDecimal’ and ‘Lo.’
The ‘input’ datatypes are for the upsertCompanySite(...)
to call in the ‘Mutation’ type.
The ‘type’ datatypes are for the return types of the ‘Mutation’ or ‘Query’ calls.
The ‘Query’ type contains the functions to read the data from the GraphQL interface.
The ‘Mutation’ type contains the functions to change the data with the GraphQL interface. The ‘CompanySiteIn’ input type is a tree of data that is posted to the interface. The interface returns the updated data in ‘CompanySiteOut.’
GraphQL Configuration
The additional types of the graphql-java-extended-scalars
library need to be configured in Spring GraphQL. That is done in the GraphQLConfig:
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
RuntimeWiringConfigurer result = wiringBuilder ->
wiringBuilder.scalar(ExtendedScalars.Date)
.scalar(ExtendedScalars.DateTime)
.scalar(ExtendedScalars.GraphQLBigDecimal)
.scalar(ExtendedScalars.GraphQLBigInteger)
.scalar(ExtendedScalars.GraphQLByte)
.scalar(ExtendedScalars.GraphQLChar)
.scalar(ExtendedScalars.GraphQLLong)
.scalar(ExtendedScalars.GraphQLShort)
.scalar(ExtendedScalars.Json)
.scalar(ExtendedScalars.Locale)
.scalar(ExtendedScalars.LocalTime)
.scalar(ExtendedScalars.NegativeFloat)
.scalar(ExtendedScalars.NegativeInt)
.scalar(ExtendedScalars.NonNegativeFloat)
.scalar(ExtendedScalars.NonNegativeInt)
.scalar(ExtendedScalars.NonPositiveFloat)
.scalar(ExtendedScalars.NonPositiveInt)
.scalar(ExtendedScalars.Object)
.scalar(ExtendedScalars.Time)
.scalar(ExtendedScalars.Url)
.scalar(ExtendedScalars.UUID);
return result;
}
The additional datatypes of the graphql-java-extended-scalars
library are registered with the ‘wiringBuilder.’
GraphQL Controllers
The ‘Query’ and ‘Mutation’ requests are implemented in the ConfigurationController and the CompanySiteController. The CompanySiteController
is shown here:
@Controller
public class CompanySiteController {
private static final Logger LOGGER =
LoggerFactory.getLogger(CompanySite.class);
private final CompanySiteService companySiteService;
private final EntityDtoMapper entityDtoMapper;
private record Selections(boolean withPolygons, boolean withRings,
boolean withLocations) {}
public CompanySiteController(CompanySiteService companySiteService,
EntityDtoMapper entityDtoMapper) {
this.companySiteService = companySiteService;
this.entityDtoMapper = entityDtoMapper;
}
@QueryMapping
public Mono<List<CompanySiteDto>> getCompanySiteByTitle(
@Argument String title, @Argument Long year,
DataFetchingEnvironment dataFetchingEnvironment) {
Selections selections = createSelections(dataFetchingEnvironment);
List<CompanySiteDto> companySiteDtos =
this.companySiteService.findCompanySiteByTitleAndYear(title,
year, selections.withPolygons(), selections.withRings(),
selections.withLocations()).stream().map(companySite ->
this.entityDtoMapper.mapToDto(companySite))
.collect(Collectors.toList());
return Mono.just(companySiteDtos);
}
private Selections createSelections(DataFetchingEnvironment
dataFetchingEnvironment) {
boolean addPolygons =
dataFetchingEnvironment.getSelectionSet().contains("polygons");
boolean addRings =
dataFetchingEnvironment.getSelectionSet().getFields().stream()
.anyMatch(sf -> "rings".equalsIgnoreCase(sf.getName()));
boolean addLocations =
dataFetchingEnvironment.getSelectionSet().getFields().stream()
.filter(sf -> "rings".equalsIgnoreCase(sf.getName()))
.flatMap(sf -> Stream.of(sf.getSelectionSet()))
.anyMatch(sf -> sf.contains("locations"));
Selections selections = new Selections(addPolygons, addRings,
addLocations);
return selections;
}
The CompanySiteController
implements the ‘CompanySite’ related methods of the GraphQL schema. It gets the CompanySiteService
and the EntityDtoMapper
injected.
The getCompanySiteByTitle(…)
method gets the arguments with the annotations that are defined in the GraphQL schema and the DataFetchingEnvironment
. The DataFetchingEnvironment
is used to call the createSelections(…)
method. The createSelections(…)
method uses the SelectionSets
to find the requested children, like ‘polygons,’ ‘rings,’ and ‘locations’ and creates a record with booleans to return the requested child types. The method findCompanySiteByTitleAndYear(…)
of the CompanySiteService
is then used to read the ‘CompanySites.’ The result entities are then mapped with the EntityDtoMapper
into DTO’s and are returned. The EntityDtoMapper
is able to handle not initialized JPA properties. Spring GraphQL filters and returns only the requested properties of the DTO’s.
GraphQL Services
The CompanySiteService implements the flexible selection structure of the data:
@Transactional
@Service
public class CompanySiteService {
private final CompanySiteRepository companySiteRepository;
private final PolygonRepository polygonRepository;
private final RingRepository ringRepository;
private final LocationRepository locationRepository;
private final DataFetcher<Iterable<CompanySite>> dataFetcherCs;
private final EntityManager entityManager;
public CompanySiteService(
CompanySiteRepository companySiteRepository,
PolygonRepository polygonRepository, RingRepository ringRepository,
LocationRepository locationRepository,
EntityManager entityManager) {
this.companySiteRepository = companySiteRepository;
this.polygonRepository = polygonRepository;
this.ringRepository = ringRepository;
this.locationRepository = locationRepository;
this.entityManager = entityManager;
}
public Collection<CompanySite> findCompanySiteByTitleAndYear(String
title, Long year, boolean withPolygons,
boolean withRings, boolean withLocations) {
if (title == null || title.length() < 2 || year == null) {
return List.of();
}
LocalDate beginOfYear = LocalDate.of(year.intValue(), 1, 1);
LocalDate endOfYear = LocalDate.of(year.intValue(), 12, 31);
title = title.trim().toLowerCase();
List<CompanySite> companySites = this.companySiteRepository
.findByTitleFromTo(title, beginOfYear, endOfYear).stream()
.peek(myCompanySite ->
this.entityManager.detach(myCompanySite)).toList();
companySites = addEntities(withPolygons, withRings, withLocations,
companySites);
return companySites;
}
private List<CompanySite> addEntities(boolean withPolygons, boolean
withRings, boolean withLocations, List<CompanySite> companySites) {
if (withPolygons) {
Map<Long, List<Polygon>> fetchPolygons =
this.fetchPolygons(companySites);
Map<Long, List<Ring>> fetchRings = !withRings ? Map.of() :
this.fetchRings(fetchPolygons.values()
.stream().flatMap(List::stream).toList());
Map<Long, List<Location>> fetchLocations = !withLocations ?
Map.of() : this.fetchLocations(fetchRings.values()
.stream().flatMap(List::stream).toList());
companySites.forEach(myCompanySite -> {
myCompanySite.setPolygons(
new HashSet<>(fetchPolygons.
getOrDefault(myCompanySite.getId(), List.of())));
if (withRings) {
myCompanySite.getPolygons()
.forEach(myPolygon -> {
myPolygon.setRings(
new HashSet<>(fetchRings.getOrDefault(
myPolygon.getId(), List.of())));
if (withLocations) {
myPolygon.getRings().forEach(myRing -> {
myRing.setLocations(
new HashSet<>(fetchLocations
.getOrDefault(myRing.getId(),
List.of())));
});
}
});
}
});
}
return companySites;
}
public Map<Long, List<Polygon>> fetchPolygons(List<CompanySite>
companySites) {
List<Polygon> polygons = this.polygonRepository
.findAllByCompanySiteIds(companySites.stream().map(cs ->
cs.getId()).collect(Collectors.toList()))
.stream().peek(myPolygon ->
this.entityManager.detach(myPolygon)).toList();
return companySites.stream().map(CompanySite::getId)
.map(myCsId -> Map.entry(findEntity(companySites,
myCsId).getId(), findPolygons(polygons, myCsId)))
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue));
}
...
}
The CompanySiteService
gets the needed repositories and the EntityManager
injected.
The method findCompanySiteByTitleAndYear(…)
checks its parameters and then prepares the parameters of the findByTitleFromTo(…)
method for the CompanySiteRepository
. The method returns the matching ‘CompanySite’ entities and uses a Stream to detach them. The method addEntities(...)
is then called to retrieve the child entities of the requested ‘CompanySite’ entities. For each child layer (such as ‘Polygons,’ ‘Rings,’ ‘Locations’) all child entities are collected. After all the parent entities are collected the children are selected in one query to avoid the ‘+1’ query problem. The child entities are also detached to support partial loading of the child entities and to support the DTO mapping.
The fetchPolygons(…)
method uses the findAllByCompanySiteIds(…)
method of the PolygonRepository
to select all the matching Polygons of all the ‘CompanySite ID’s.’ The Polygons are returned in a map with the ‘CompanySite ID’ as the key and the Polygon entity list as the value.
GraphQL Repositories
To support the loading of the child entities by layer. The child entities have to be selected by parent ‘ID.’ That is, for example, done in the JPAPolygonRepository:
public interface JpaPolygonRepository extends JpaRepository<Polygon,
Long>, QuerydslPredicateExecutor<Polygon> {
@Query("select p from Polygon p inner join p.companySite cs
where cs.id in :ids")
List<Polygon> findAllByCompanySiteIds(@Param("ids") List<Long> ids);
}
The findAllByCompanySiteIds(…)
method creates a JQL query that joins the ‘CompanySite’ entity and selects all Polygons for the ‘CompanySite IDs.’ That means one query for all Polygons. Four layers (such as ‘CompanySite,’ ‘Polygon,’ ‘Ring,’ and ‘Location’) equal four queries.
Conclusion Backend
To avoid the ‘+1’ problem with entity loading and to load only the needed entities, this layered approach was needed. A REST endpoint that knows how many child layers have to be loaded can be implemented more efficiently (one query). The price to pay for that efficiency are more endpoints.
Conclusion
REST and GraphQL both have advantages and disadvantages.
- Because the returned results are less flexible, REST can support more logic at the endpoints more easily.
- GraphQL supports the specification of the requested results. That can be used to request more flexible results that can be smaller but needs a flexible backend to efficiently support all possible result shapes.
Each project needs to decide what architecture better fits the given requirements. For that decision, the requirements and the estimated development effort for the frontend and backend implementations need to be considered.
Published at DZone with permission of Sven Loesekann. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments