Query查詢
在一個schema上執行查詢,需要首先創建一個GraphQL
對象,然后調用該對象的execute()
方法
GraphQL在執行結束后返回一個ExecutionResult
對象,其中包含查詢的數據(data字段)或錯誤信息(errors字段)。
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.build();
GraphQL graphQL = GraphQL.newGraphQL(schema)
.build();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
.build();
ExecutionResult executionResult = graphQL.execute(executionInput);
Object data = executionResult.getData();
List<GraphQLError> errors = executionResult.getErrors();
Data Fetcher
每個GraphQL中的field(字段)都會綁定一個DataFetcher。在其他的GraphQL實現中,也稱DataFetcher為Resolver。
一般,我們可以使用PropertyDataFetcher對象,從內存中的POJO對象中提取field的值。如果你沒有為一個field顯式指定一個DataFetcher,那么GraphQL默認會使用PropertyDataFetcher與該field進行綁定。
但對於最頂層的領域對象(domain object)查詢來說,你需要定義一個特定的data fetcher。頂層的領域對象查詢,可能會包含數據庫操作,或通過HTTP協議與其他系統進行交互獲得相應數據。
GraphQL - Java並不關心你是如何獲取領域對象數據的,這是業務代碼中需要考慮的問題。它也不關心在獲取數據時需要怎樣的認證方式,你需要在業務層代碼中實現這部分邏輯。
一個簡單的Data Fetcher示例如下:
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return fetchUserFromDatabase(environment.getArgument("userId"));
}
};
每個DataFetcher的方法中,都會傳入一個DataFetchingEnvironment對象。這個對象中包含了當前正在被請求的field,field所關聯的請求參數argument,以及其他信息(例如,當前field的上層field、當前查詢的root對象或當前查詢的context對象等)。
在上面的例子中,GraphQL會在data fetcher返回執行結果前一直等待,這是一種阻塞的調用方式。也可以通過返回data相關的CompletionStage對象,將DataFetcher的調用異步化,實現異步調用。
數據獲取時產生的異常
如果在GraphQL的DataFetcher執行過程中產生了異常,在GraphQL的執行策略下, 將生成一個ExceptioinWhileDataFetching錯誤對象,並將它添加到返回的ExecutionResult對象的errors列表字段當中。GraphQL允許返回部分成功的數據,並帶上異常信息。
正常的異常處理邏輯如下:
public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(SimpleDataFetcherExceptionHandler.class);
@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
Throwable exception = handlerParameters.getException();
SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
ExecutionPath path = handlerParameters.getPath();
ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
handlerParameters.getExecutionContext().addError(error);
log.warn(error.getMessage(), exception);
}
}
如果拋出的異常是一個GraphqlError對象,那么它會將異常信息和擴展屬性轉換到ExceptionWhileDataFetching對象。可以把自己的錯誤信息,放到GraphQL的錯誤列表當中返回給調用方。
例如,假設data fetcher拋出了如下異常,那么foo和fizz屬性會包含在graphql error對象當中。
class CustomRuntimeException extends RuntimeException implements GraphQLError {
@Override
public Map<String, Object> getExtensions() {
Map<String, Object> customAttributes = new LinkedHashMap<>();
customAttributes.put("foo", "bar");
customAttributes.put("fizz", "whizz");
return customAttributes;
}
@Override
public List<SourceLocation> getLocations() {
return null;
}
@Override
public ErrorType getErrorType() {
return ErrorType.DataFetchingException;
}
}
可以編寫自己的DataFetcherExceptionHandler異常處理器改變它的行為,只需要在執行策略中注冊一下。
例如,上述代碼記錄了底層的異常和調用棧信息,如果你不希望這些信息出現在輸出的錯誤列表中,可以用一下的方法實現。
DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() {
@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
//
// do your custom handling here. The parameters have all you need
}
};
ExecutionStrategy executionStrategy = new AsyncExecutionStrategy(handler);
返回數據和異常
也可以在一個DataFetcher中同時返回數據和多個error信息,只需要讓DataFetcher返回DataFetcherResult對象或CompletableFuture包裝后的DataFetcherResult對象即可。
在某些場景下,例如DataFetcher需要從多個數據源或其他的GraphQL系統中獲取數據時,其中任一環節都可能產生錯誤。使用DataFetcherResult包含data和期間產生的所有error信息,比較常見。
下面的示例中,DataFetcher從另外的GraphQL系統中獲取數據,並返回執行的data和errors信息。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
Map response = fetchUserFromRemoteGraphQLResource(environment.getArgument("userId"));
List<GraphQLError> errors = response.get("errors")).stream()
.map(MyMapGraphQLError::new)
.collect(Collectors.toList();
return new DataFetcherResult(response.get("data"), errors);
}
};
序列化返回結果為json格式
通常,使用Jackson或GSON的json序列化庫,將返回查詢結果序列化為json格式返回。然而對於如何序列化數據,序列化為JSON后會保留哪些信息,取決於序列化庫自身。例如,對於null結果是否出現在序列化后的json數據當中,不同的序列化庫有不同的默認策略。需要手動指定json mapper來定義。
為了保證可以100%獲取一個符合graphql規范的json結果,可以在返回結果result上調用toSpecification,然后將數據以json格式返回。
ExecutionResult executionResult = graphQL.execute(executionInput);
Map<String, Object> toSpecificationResult = executionResult.toSpecification();
sendAsJson(toSpecificationResult);
Mutation(更新)
首先,需要定義一個支持輸入參數的GraphQLObjectType
類型,該類型也是Mutation方法的參數類型。這些參數會在data fetcher調用時,更新GraphQL系統內部的領域數據信息(添加、修改或刪除)。
mutation的執行調用示例如下:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
在mutation方法執行過程中需要傳遞參數,例如實例中,需要傳遞$ep和$review變量。
在Java代碼中,可以使用如下方式創建type,並綁定這個mutation操作。
GraphQLInputObjectType episodeType = newInputObject()
.name("Episode")
.field(newInputObjectField()
.name("episodeNumber")
.type(Scalars.GraphQLInt))
.build();
GraphQLInputObjectType reviewInputType = newInputObject()
.name("ReviewInput")
.field(newInputObjectField()
.name("stars")
.type(Scalars.GraphQLString)
.name("commentary")
.type(Scalars.GraphQLString))
.build();
GraphQLObjectType reviewType = newObject()
.name("Review")
.field(newFieldDefinition()
.name("stars")
.type(GraphQLString))
.field(newFieldDefinition()
.name("commentary")
.type(GraphQLString))
.build();
GraphQLObjectType createReviewForEpisodeMutation = newObject()
.name("CreateReviewForEpisodeMutation")
.field(newFieldDefinition()
.name("createReview")
.type(reviewType)
.argument(newArgument()
.name("episode")
.type(episodeType)
)
.argument(newArgument()
.name("review")
.type(reviewInputType)
)
)
.build();
GraphQLCodeRegistry codeRegistry = newCodeRegistry()
.dataFetcher(
coordinates("CreateReviewForEpisodeMutation", "createReview"),
mutationDataFetcher()
)
.build();
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.mutation(createReviewForEpisodeMutation)
.codeRegistry(codeRegistry)
.build();
注意,輸入參數只能是GraphQLInputObjectType類型,不能是可以作為輸出類型的GraphQLObjectType。
另外,Scalar類型比較特殊,可以同時作為輸入參數類型和輸出類型。
Mutation操作綁定的data fetcher可以執行這個mutation,並返回輸出類型的數據信息:
private DataFetcher mutationDataFetcher() {
return new DataFetcher() {
@Override
public Review get(DataFetchingEnvironment environment) {
//
// The graphql specification dictates that input object arguments MUST
// be maps. You can convert them to POJOs inside the data fetcher if that
// suits your code better
//
// See http://facebook.github.io/graphql/October2016/#sec-Input-Objects
//
Map<String, Object> episodeInputMap = environment.getArgument("episode");
Map<String, Object> reviewInputMap = environment.getArgument("review");
//
// in this case we have type safe Java objects to call our backing code with
//
EpisodeInput episodeInput = EpisodeInput.fromMap(episodeInputMap);
ReviewInput reviewInput = ReviewInput.fromMap(reviewInputMap);
// make a call to your store to mutate your database
Review updatedReview = reviewStore().update(episodeInput, reviewInput);
// this returns a new view of the data
return updatedReview;
}
};
}
如上所示,方法調用了數據庫操作變更了后端的數據存儲信息,然后返回一個Review類型對象返回給mutation的調用方。
異步執行
graphql-java使用了完全異步化的執行策略,調用executeAsync()后,返回CompleteableFuture
對象
GraphQL graphQL = buildSchema();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
.build();
CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
promise.thenAccept(executionResult -> {
// here you might send back the results as JSON over HTTP
encodeResultToJsonAndSendResponse(executionResult);
});
promise.join();
使用CompletableFuture對象,可以指定該執行結果結束后需要出發的后續行為或操作,最后調用.join()方法等待執行完成。
實際上,使用GraphQL Java執行execute的同步操作,也是在調用異步的executeAsync方法之后,再調用join方法實現的。
ExecutionResult executionResult = graphQL.execute(executionInput);
// the above is equivalent to the following code (in long hand)
CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
ExecutionResult executionResult2 = promise.join();
如果DataFetcher返回了CompletableFuture
下面的代碼中使用了Java中的ForkJoinPool.commonPool線程池,提供異步執行操作流程。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
CompletableFuture<User> userPromise = CompletableFuture.supplyAsync(() -> {
return fetchUserViaHttp(environment.getArgument("userId"));
});
return userPromise;
}
};
上述代碼在Java8中也可以重構如下:
DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(
() -> fetchUserViaHttp(environment.getArgument("userId")));
Graphql - Java會保證所有的CompletableFuture對象組合執行,並依照GraphQL規范返回執行結果。
在GraphQL - Java中也可以使用AsyncDataFetcher.async(DataFetcher
DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));
執行策略
在執行query或mutation時,GraphQL Java引擎會使用ExecutionStrategy接口的具體實現類(執行策略)。GraphQL - Java提供了一些策略,你也可以編寫自己的執行策略。
可以在創建GraphQL
對象時中綁定執行策略。
GraphQL.newGraphQL(schema)
.queryExecutionStrategy(new AsyncExecutionStrategy())
.mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
.build();
實際上,上述代碼等價於不指定ExecutionStrategy的默認策略。
AsyncExecutionStrategy
query操作的默認執行策略是AsyncExecutionStrategy。在這個執行策略下,GraphQL Java引擎將field的返回結果包裝為CompleteableFuture對象(如果返回結果本身為CompletableFuture對象,則不處理),哪個field的值獲取操作先完成並不重要。
若data fetcher調用本身返回的就是CompletationStage類型,則可以最大化異步調用的性能。
對於如下的query:
query {
hero {
enemies {
name
}
friends {
name
}
}
}
AsyncExecutionStrategy會在獲取friends字段值的同時,調用獲取enemies字段值的方法。而不會在獲取enemies之后獲取friends字段的值,以提升效率。
在執行結束后,GraphQL Java會將查詢結果按照請求的順序進行整合。查詢結果遵循Graphql規范,並且返回的field對象按照查詢的field字段的順序返回。
執行過程中,僅僅是field字段的執行順序是任意的,返回的查詢結果依然是順序的。
AsyncSerialExecutionStrategy
GraphQL 規范要求mutation操作必須按照query的field順序依次執行。
因此,AsyncSerialExecutionStrategy是mutation的默認策略,並且它會保證每個field在下一個field操作開始之前完成。
你仍然可以在mutation類型的data fetcher中返回CompletionStage,但它們只會順序依次執行。
SubscriptionExecutionStrategy
略。
Query緩存
在GraphQL Java執行查詢之前,查詢語句首先應該進行解析和驗證,這個過程有時候會非常耗時。
為了避免重復遍歷、驗證查詢語句,GraphQL.Builder允許引入PreparedDocumentProvider,來重用相同query語句的Document解析實例。
這個過程只是對Document進行緩存,並未對查詢的執行結果進行緩存。
Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build(); (1)
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(cache::get) (2)
.build();
- 創建一個cache的實例,示例代碼使用的是caffeine的緩存方案。
- PreparedDocumentProvider是一個FunctionInterface(Java8特性),僅僅提供了一個get方法。
若開啟了緩存,那么查詢語句中不能顯式的拼接查詢條件的值。而應該以變量的方式進行傳遞。例如:
query HelloTo {
sayHello(to: "Me") {
greeting
}
}
這個查詢語句,重寫如下:
query HelloTo($to: String!) {
sayHello(to: $to) {
greeting
}
}
# 傳入參數如下:
{
"to": "Me"
}