Projections are a way for a client to request only specific fields from an object instead of the entire object. Using projections when the client needs a few fields from an object is a good way to self-document the code, reduce payload of responses, and even allow the server to relax an otherwise time consuming computation or IO operation. You can read more about projections at Projections.
Clients can project the entity object(s) in a response. For example, one can project:
For resource methods returning CollectionResult, the Metadata and Paging that is sent back to the client may also be projected.
Using projections in Java code relies heavily on PathSpec
objects,
which represent specific fields of an object. To get a PathSpec
of a
field bar of a RecordTemplate object Foo, you would write the
following:
PathSpec pathSpec = Foo.fields().bar();
For Paging projection, here is an example on how to get the PathSpec
of the total field:
PathSpec pathSpec = CollectionMetadata.fields().total();
It is not possible to set projections for non-RecordTemplate objects.
Projections are set by the request builder. To set a request projection for entity objects in the response, create your builder as you normally would and then add your projection to it:
builder.fields(pathSpec);
the fields()
method can take as arguments any number of PathSpecs, or
an array of them.
builder.fields(pathSpec1, pathSpec2, pathSpec3);
builder.fields(pathSpecArray);
This will create a positive projection for your given fields. The
request will only return fields that you have specified with
.fields(...)
.
Similarly, you can do the same for custom Metadata projection and Paging
projection for resource methods that return CollectionResult
, for
example:
builder.metadataFields(pathSpec1, pathSpec2);
builder.pagingFields(CollectionMetadata.fields().total());
AUTOMATIC
ProjectionIf you choose to examine and apply projections manually, or if you
simply would like to disable them for performance optimization, you can
turn off the framework’s AUTOMATIC
projection processing.
This can be done by setting the “projection mode” to MANUAL
on the
ResourceContext:
//For entity objects in the response
getContext().setProjectionMode(ProjectionMode.MANUAL);
//For custom Metadata projection (CollectionResult only)
getContext().setMetadataProjectionMode(ProjectionMode.MANUAL);
For example:
public Greeting get(Long key)
{
MaskTree mask = context.getProjectionMask();
if (mask != null)
{
// client has requested a projection of the entity
getContext().setProjectionMode(ProjectionMode.MANUAL); // since we’re manually applying the projection
// manually examine the projection and apply it entity before returning
// here we can take advantage of the information the projection provides to only load the data the
// client requested
}
else
{
// client is requesting the full entity
// construct and return the full entity
}
}
and in the case of CollectionResult, you could do the following as well:
@Finder("myFinder")
public CollectionResult<SomeEntity, SomeCustomEntity> myFinderResourceMethod(
final @PagingContextParam PagingContext ctx,
final @ProjectionParam MaskTree entityObjectProjection,
final @MetadataProjectionParam MaskTree metadataProjection,
final @PagingProjectionParam MaskTree pagingProjection)
{
final List<SomeEntity> responseList = new ArrayList<>();
if (entityObjectProjection != null)
{
// client has requested a projection of the entity
getContext().setProjectionMode(ProjectionMode.MANUAL); // since we’re manually applying the projection
// manually examine the projection and apply it entity before returning
// here we can take advantage of the information the projection provides to only load the data the
// client requested
responseList.addAll(fetchFiltereredEntities());
}
else
{
// client is requesting the full entities
// construct the full entities
responseList.addAll(fetchEntities());
}
final SomeCustomEntity customEntity;
if (metadataProjection != null)
{
// client has requested a projection of the custom metadata
getContext().setMetadataProjectionMode(ProjectionMode.MANUAL); // since we’re manually applying the meta data projection
// manually examine the projection and apply it entity before returning
// here we can take advantage of the information the projection provides to only load the data the
// client requested
customEntity = fetchSomeFilteredCustomEntity();
}
else
{
// client is requesting the full metadata entity
// construct and return the full metadata
customEntity = fetchSomeCustomEntity();
}
final Integer total;
if (pagingProjection != null)
{
// client has requested a projection of the paging information
// since the rest.li framework will always automatically project paging,
// we can still selectively calculate the total based on the path spec
if(pagingProjections.getOperations.get(CollectionMetadata.fields().total()) == MaskOperation.POSITIVE_MASK_OP)
{
total = calculateTimeConsumingTotal();
}
}
else
{
total = null;
}
return new CollectionResult(responseList, total, customEntity);
}
Note that Paging projection is always automatically applied by the
Rest.li framework if there is a request by the client to do so. This is
because it is the Rest.li framework who is responsible for constructing
the pagination (CollectionMetadata
) which includes items such as the
next/prev links . The MaskTree
provided for Paging is simply provided
as a reference to the resource method with the most common use case
being whether or not to pass null
for the total in the construction of
CollectionResult
.
No. If you want a large number of fields, you will need to include them
all in the .fields(...)
method
call.
Yes, the simplest way to is to use the RecordTemplate.fields()
method
to help construct the appropriate pathspec to pass to the builder’s
.fields(...)
method call. For example:
new ExampleBuilders(options).get()
.id(id)
.fields(RootRecord.fields().message().id())
.build()
Applies projection on a GET request to a resource where the message
field of RootRecord.pdl
is a record type called Message.pdl
, and
only the id
fields of the message is being projected. The same logic
can be applied to RecordTemplates within custom Metadata and Paging
projection.
In general, examining a request’s projections on the server side will not be necessary. When the server returns an object to the client, the REST framework will take care of stripping all unrequested fields. It is not necessary for the server to examine the projection and strip fields itself.
However, it is possible for the server to examine a request’s projection.
MaskTree entityProjection = getContext().getProjectionMask();
MaskTree metadataProjection = getContext().getMetadataProjectionMask();
MaskTree pagingProjection = getContext().getPagingProjectionMask();
Or, if you are using free-form resources, you can get the same
MaskTree
by having it injected in, for example:
@RestMethod.Get
public Greeting get(Long key, @ProjectionParam MaskTree projection)
{
// …
}
This will get you all possible projections of a request. If there were
no projections available, the respective MaskTrees
would be null
.
Note that the use of these annotations is mandatory if you specify
MaskTrees
in your method signatures.
If there were projections, you can check the status of each field.
MaskOperation mask = projections.getOperations.get(pathSpec);
if (mask == MaskOperation.POSITIVE_MASK_OP)
{
// field is requested.
}
else
{
// field is not requested
}
MaskOperation totalMask = pagingProjections.getOperations.get(CollectionMetadata.fields().total());
if (totalMask == MaskOperation.POSITIVE_MASK_OP)
{
// the total field in pagination is requested.
}
else
{
// total is not requested
}
You can use this information in whatever way you wish to. For example,
resource methods may choose to exclude the calculation of ‘total’
(thereby passing null
for total into CollectionResult
) if the client
decided they didn’t need it.