- Writing your Own Custom Scalars
- Coercing values
- Validation of input and output
- Example implementation
- 使用自定义标量
graphql类型系统的叶节点称为标量。一旦达到标量类型,就无法进一步下降到类型层次结构中。标量类型旨在表示不可分的值。
graphql规范指出,所有实现都必须具有以下标量类型。
- String 又名 GraphQLString - 一个UTF‐8字符序列
- Boolean 又名 GraphQLBoolean - true 或 false
- Int 又名 GraphQLInt - 一个有符号的32位整数
- Float 又名 GraphQLFloat - 一个有符号双精度浮点数
- ID 又名 GraphQLID - 唯一标识符,以与String相同的方式序列化;但是,将其定义为ID表示它不是人类可读的。
graphql-java添加了以下标量类型,这些标量类型在基于Java的系统中很有用
- Long GraphQLLong - java.lang.Long
- Short GraphQLShort - java.lang.Short
- Byte GraphQLByte - java.lang.Byte
- BigDecimal GraphQLBigDecimal - java.math.BigDecimal
- BigInteger GraphQLBigInteger - java.math.BigInteger
graphql.Scalars类包含所提供标量类型的单例实例
Writing your Own Custom Scalars
您可以编写自己的自定义标量实现。这样,您将负责在运行时强制限制值,我们将在稍后解释。
想象我们决定需要一个电子邮件标量类型。它将电子邮件地址作为输入和输出。
我们将为此创建一个单例graphql.schema.GraphQLScalarType实例。
public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
//接受一个Java对象并将其转换为该标量的输出形式
@Override
public Object serialize(Object dataFetcherResult) {
return serializeEmail(dataFetcherResult);
}
//接受变量输入对象并转换为Java运行时表示形式
@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}
//将AST文字graphql.language.Value作为输入并转换为Java运行时表示形式
@Override
public Object parseLiteral(Object input) {
return parseEmailFromAstLiteral(input);
}
});
Coercing values
任何自定义标量实现中的实际工作都是实现graphql.schema.Coercing。
您的自定义标量代码必须处理2种形式的输入(parseValue/parseLiteral)和1种形式的输出(序列化)。
想象一下这个查询,它使用变量,AST文字并输出我们的标量类型电子邮件。
mutation Contact($mainContact: Email!) {
makeContact(mainContactEmail: $mainContact, backupContactEmail: "backup@company.com") {
id
mainContactEmail
}
}
我们的自定义电子邮件标量将
- 通过parseValue调用以将$mainContact变量值转换为运行时对象
- 通过parseLiteral调用以将AST graphql.language.StringValue “backup@company.com” 转换为运行时对象
- 通过序列化调用,以将mainContactEmail的运行时表示形式转换为可用于输出的表单
Validation of input and output
该方法可以验证接收到的输入是否有意义。例如,我们的电子邮件标量将尝试验证输入和输出确实是电子邮件地址。
graphql.schema.Coercing的JavaDoc方法协定如下
- 只允许从序列化抛出graphql.schema.CoercingSerializeException。这表明该值不能序列化为适当的形式。您不得允许其他运行时异常转义此方法以获取正常的graphql行为进行验证。您必须返回非null值。
- 仅允许从parseValue引发graphql.schema.CoercingParseValueException。这表明该值不能解析为适当形式的输入。您不得允许其他运行时异常转义此方法以获取正常的graphql行为进行验证。您必须返回非null值。
- 仅允许从parseLiteral引发graphql.schema.CoercingParseLiteralException。这表明AST值不能解析为适当形式的输入。您不得允许任何运行时异常转义此方法,以获取正常的graphql行为进行验证。
有些人试图依靠运行时异常进行验证,并希望它们以graphql错误的形式出现。然而并发如此,您必须遵循Coercing方法协定,以允许graphql-java引擎根据有关标量类型的graphql规范工作。
Example implementation
以下是我们想象中的email标量类型的一个非常粗略的实现,以向您展示如何实现这种标量的Coercing方法。
public static class EmailScalar {
public static final GraphQLScalarType Email = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
@Override
public Object serialize(Object dataFetcherResult) {
return serializeEmail(dataFetcherResult);
}
@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}
@Override
public Object parseLiteral(Object input) {
return parseEmailFromAstLiteral(input);
}
});
private static boolean looksLikeAnEmailAddress(String possibleEmailValue) {
// 这个正则不是标准的验证邮件地址的那个
return Pattern.matches("^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", possibleEmailValue);
}
private static Object serializeEmail(Object dataFetcherResult) {
String possibleEmailValue = String.valueOf(dataFetcherResult);
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
} else {
throw new CoercingSerializeException("Unable to serialize " + possibleEmailValue + " as an email address");
}
}
private static Object parseEmailFromVariable(Object input) {
if (input instanceof String) {
String possibleEmailValue = input.toString();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseValueException("Unable to parse variable value " + input + " as an email address");
}
private static Object parseEmailFromAstLiteral(Object input) {
if (input instanceof StringValue) {
String possibleEmailValue = ((StringValue) input).getValue();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseLiteralException(
"Value is not any email address : '" + String.valueOf(input) + "'"
);
}
}
使用自定义标量
使用上面定义的Email类型完成请求与相应
//自定义标量,......表示省略
......
//在StarWarsWiring中定义(或直接使用,主要为了使用依赖注入)
GraphQLScalarType Email = EmailScalar.Email;
......
//在织入时绑定
private GraphQLSchema buildSchema(String sdl) {
//读取从resources下加载解析的gql schema文件的字符串
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
//创建运行时类型织入
RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
//动态映射
//运行时织入包含DataFetcher、TypeResolvers和自定义Scalar,它们是制作完全可执行的schema所必需的。
builder.type(newTypeWiring("Query")
.dataFetcher("hero", starWarsWiring.heroDataFetcher)
.dataFetcher("human", starWarsWiring.humanDataFetcher)
.dataFetcher("humans", starWarsWiring.humansDataFetcher)
.dataFetcher("droid", starWarsWiring.droidDataFetcher));
builder.type(newTypeWiring("Human").dataFetcher("friends", starWarsWiring.friendsDataFetcher));
builder.type(newTypeWiring("Droid").dataFetcher("friends", starWarsWiring.friendsDataFetcher));
builder.type(newTypeWiring("Character").typeResolver(starWarsWiring.characterTypeResolver));
builder.type(newTypeWiring("Episode").enumValues(starWarsWiring.episodeResolver));
builder.scalar(starWarsWiring.Email);//注入StarWarsWiring中实现的自定义的标量
SchemaGenerator schemaGenerator = new SchemaGenerator();
//创建gql schema对象,构造可执行请求的gql
return schemaGenerator.makeExecutableSchema(typeRegistry, builder.build());
}
在starWarsSchemaAnnotated.graphqls文件修改为
scalar Email
......
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
homePlanet: String
secretBackstory : String @deprecated(reason : "We have decided that this is not canon")
# 添加一个属性,使用了自定义标量
email: Email!
}
......
修改Java POJO,为Human类增加String类型 email字段
使用如下查询
{
humans {
name
email
}
}
返回信息(开了追踪所以有extensions属性)
{
"data": {
"humans": [
{
"name": "Luke Skywalker",
"email": "dreamylost@outlook.com"
},
{
"name": "Darth Vader",
"email": "dreamylost@outlook.com"
},
{
"name": "Han Solo",
"email": "dreamylost@outlook.com"
},
{
"name": "Leia Organa",
"email": "dreamylost@outlook.com"
}
]
},
"extensions": {
"tracing": {
"version": 1,
"startTime": "2020-04-03T09:25:00.753Z",
"endTime": "2020-04-03T09:25:00.757Z",
"duration": 3874865,
"parsing": {
"startOffset": 491647,
"duration": 404205
},
"validation": {
"startOffset": 754108,
"duration": 231721
},
"execution": {
"resolvers": [
{
"path": [
"humans"
],
"parentType": "Query",
"returnType": "[Human]",
"fieldName": "humans",
"startOffset": 1317556,
"duration": 506334
},
{
"path": [
"humans",
0,
"name"
],
"parentType": "Human",
"returnType": "String!",
"fieldName": "name",
"startOffset": 2007874,
"duration": 58120
},
{
"path": [
"humans",
0,
"email"
],
"parentType": "Human",
"returnType": "Email!",
"fieldName": "email",
"startOffset": 2199262,
"duration": 58219
},
{
"path": [
"humans",
1,
"name"
],
"parentType": "Human",
"returnType": "String!",
"fieldName": "name",
"startOffset": 2507644,
"duration": 36967
},
{
"path": [
"humans",
1,
"email"
],
"parentType": "Human",
"returnType": "Email!",
"fieldName": "email",
"startOffset": 2643613,
"duration": 27507
},
{
"path": [
"humans",
2,
"name"
],
"parentType": "Human",
"returnType": "String!",
"fieldName": "name",
"startOffset": 2880688,
"duration": 34660
},
{
"path": [
"humans",
2,
"email"
],
"parentType": "Human",
"returnType": "Email!",
"fieldName": "email",
"startOffset": 3097616,
"duration": 47421
},
{
"path": [
"humans",
3,
"name"
],
"parentType": "Human",
"returnType": "String!",
"fieldName": "name",
"startOffset": 3369605,
"duration": 30026
},
{
"path": [
"humans",
3,
"email"
],
"parentType": "Human",
"returnType": "Email!",
"fieldName": "email",
"startOffset": 3505375,
"duration": 45639
}
]
}
},
"dataloader": {
"overall-statistics": {
"loadCount": 8,
"loadErrorCount": 0,
"loadErrorRatio": 0.0,
"batchInvokeCount": 1,
"batchLoadCount": 4,
"batchLoadRatio": 0.5,
"batchLoadExceptionCount": 0,
"batchLoadExceptionRatio": 0.0,
"cacheHitCount": 4,
"cacheHitRatio": 0.5
},
"individual-statistics": {
"characters": {
"loadCount": 8,
"loadErrorCount": 0,
"loadErrorRatio": 0.0,
"batchInvokeCount": 1,
"batchLoadCount": 4,
"batchLoadRatio": 0.5,
"batchLoadExceptionCount": 0,
"batchLoadExceptionRatio": 0.0,
"cacheHitCount": 4,
"cacheHitRatio": 0.5
}
}
}
}
}
完整代码 https://github.com/jxnu-liguobin/springboot-examples
文档信息
- 本文作者:梦境迷离
- 本文链接:https://blog.dreamylost.cn/graphqljava/GraphqlJava-Scalars.html
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)