GraphQL的简单应用

GraphQL是为API而生的专用查询语言。

一般来说,前后端API交互采用RESTful风格设计。

特点:

所有事务抽象为资源,每个资源有唯一标识,操作无状态,操作不改变唯一标识。

缺点:

一个接口一个资源,兼容性差,大部分情况下后端对接口内容有决策权。

GraphQL是需求驱动的,后端定义数据范围,前端定义响应内容。

request

{
    hero {
      name
      friends {
        name
      }
    }
}

response

{
    "data": {
        "hero": {
            "name": "RD",
            "friends": [{
                "name": "Luke"
            }, {
                "name": "Leia"
            }]
        }
    }
}

一、实例

GraphQL有三要素组成:Schema,Query,DataFetcher。

其中,Schema和DataFetcher定义在服务端,Query在客户端。

Schema

type Query {
    bookById(id: ID): Book
}

type Book {
    id: ID
    name: String
    pageCount: Int
    author: Author
}

type Author {
    id: ID
    firstName: String
    lastName: String
}

DataFetcher

private RuntimeWiring buildWiring() {
    return RuntimeWiring
         .newRuntimeWiring()
         .type(newTypeWiring("Query")
             .dataFetcher("bookById", 
             graphQLDataFetchers.
             getBookById()))
         .type(newTypeWiring("Book")
             .dataFetcher("author", 
             graphQLDataFetchers
             .getAuthor()))
         .build();
}

Query

{
     bookById(id: "book-1") {
         id
         name
         pageCount
         author {
             firstName
             lastName
         }
     }
}

定义好三要素后,将Schema加载为项目实例。

@Component
public class GraphQLProvider {
    private GraphQL graphQL;
    private GraphQLSchema graphQLSchema;

    @Resource
    private GraphQLDataFetchers graphQLDataFetchers;

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }

    @PostConstruct
    public void init() throws IOException {
        Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build();
        PreparsedDocumentProvider preparsedDocumentProvider =
                (ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> function) -> {
                    Function<String, PreparsedDocumentEntry> mapCompute = key -> function.apply(executionInput);
                    return cache.get(executionInput.getQuery(), mapCompute);
                };

        URL url = Resources.getResource("schema.graphqls");
        String sdl = Resources.toString(url, Charsets.UTF_8);
        graphQLSchema = buildSchema(sdl);
        this.graphQL = GraphQL.newGraphQL(graphQLSchema)
                .preparsedDocumentProvider(preparsedDocumentProvider)
                .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy())
                .build();
    }

    public GraphQLSchema getGraphQLSchema() {
        return graphQLSchema;
    }

    private GraphQLSchema buildSchema(String sdl) {
        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
        RuntimeWiring runtimeWiring = buildWiring();
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
    }

    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
                .type("Query", typeWiring -> typeWiring
                        .dataFetcher("bookById", graphQLDataFetchers.getBookById())
                )
                .type(newTypeWiring("Book")
                        .dataFetcher("author", graphQLDataFetchers.getAuthor()))
                .build();
    }

}

DataFetcher的具体实现

@Component
public class GraphQLDataFetchers {
    private final static BookPublisher BOOK_PUBLISHER = new BookPublisher();

    @Resource
    private BookRepo bookRepo;
    @Resource
    private AuthorRepo authorRepo;

    public DataFetcher getBookById() {
        return dataFetchingEnvironment -> {
            Integer id = dataFetchingEnvironment.getArgument("id");
            return bookRepo.findById(id);
        };
    }

    public DataFetcher getAuthor() {
        return dataFetchingEnvironment -> {
            Book book = dataFetchingEnvironment.getSource();
            return book.getAuthor();
        };
    }
}

Query部分通过Postman模拟。

postman-graphql-1

一个相对完整的流程就完成了。

1.1定义Schema

Schema有两种定义方式。

配置文件。

type Foo {
 bar: String
}

代码:

GraphQLObjectType fooType = newObject()
 .name("Foo")
 .field(newFieldDefinition()
 .name("bar")
 .type(GraphQLString))
 .build();

Schema的字段类型有:

Scalar,Object,Interface,Union,InputObject,Enum。

其中,Scalar指的是:

GraphQLString , GraphQLBoolean , GraphQLInt , GraphQLFloat , GraphQLID, GraphQLLong,GraphQLShort,GraphQLByte, GraphQLBigDecimal,GraphQLBigInteger

一些实例如下:

enum Episode {
    NEWHOPE
    EMPIRE
    JEDI
}

interface Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
}

type Human implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    homePlanet: String
}

type Droid implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    primaryFunction: String
}

type Cat {
    name: String
    lives: Int
}

type Dog {
    name: String
    bonesOwned: Int
}

union Pet = Cat | Dog

1.2mutation实现增删改

Schema

type Mutation {
  addBook(name: String, pageCount: Int, authorId: Int): Book
  updateBookName(id: Int, name: String): Book
}
    private RuntimeWiring buildWiring() {
        return RuntimeWiring.newRuntimeWiring()
                .type(newTypeWiring("Mutation")
                        .dataFetcher("addBook", graphQLDataFetchers.addBook()))
                .type(newTypeWiring("Mutation")
                        .dataFetcher("updateBookName", graphQLDataFetchers.updateBookName()))
                .build();
    }

DataFetcher

    public DataFetcher addBook() {
        return dataFetchingEnvironment -> {
            Integer authorId = dataFetchingEnvironment.getArgument("authorId");
            Integer pageCount = dataFetchingEnvironment.getArgument("pageCount");
            String name = dataFetchingEnvironment.getArgument("name");
            Author author = authorRepo.findById(authorId).orElse(null);
            Book book = new Book(name, pageCount, author);
            bookRepo.save(book);
            return book;
        };
    }

Query

mutation {
     addBook ( name : "book1", pageCount : 10, authorId: 2 ) { 
         id
         name
         author {
             firstName
             lastName
         }
     }
 }

Mutation和Query类似于Get和Post,是⼀种约定, 使⽤Query修改数据也是可⾏的。

1.3DataFetcher

默认的DataFetcher:PropertyDataFetcher,通过名称匹配字段。

即使竭⼒避免,也难免出现类似 desc 和 description 这样字段不匹配的情况。

可以定义别名alias来统一字段。

Query

query {
 bookById(id: 1) {
     id
     bookName
     count: pageCount
 }
}

Schema

directive @fetch(from : String!) on FIELD_DEFINITION

type Product {
    id : ID
    name : String
    description : String @fetch(from:"desc")
}

在三要素实例中注意到 DataFetchingEnvironment。

    public DataFetcher getBookById() {
        return dataFetchingEnvironment -> {
            Integer id = dataFetchingEnvironment.getArgument("id");
            return bookRepo.findById(id);
        };
    }

它包含要获取的字段,向该字段提供了哪些参数以及其他信息,例如字段的类型,其⽗类型, 查询根对象或查询上下⽂对象。如:

    public DataFetcher getAuthor() {
        return dataFetchingEnvironment -> {
            Book book = dataFetchingEnvironment.getSource();
            return book.getAuthor();
        };
    }

⽐较常⽤的还有:getArguments(), getRoot(), getContext(), getExecutionId() 等等

1.4Query实例

参数传递

{
 human(id: "1000") {
     name
     height
 }
}

片段复用

{
 leftComparison: hero(episode: EMPIRE) {
     ...comparisonFields
 }
 rightComparison: hero(episode: JEDI) {
     ...comparisonFields
 }
}
fragment comparisonFields on Character {
     name
     appearsIn
     friends {
         name
     }
}

给查询命名

query HeroNameAndFriends {
     hero {
         name
         friends {
             name
         }
     }
}

传递变量

postman-graphql-2

@include @skip

postman-graphql-3

 通过指令@include 决定是否包含某些内容,使⽤场景如:预览和详情。

内联片段

postman-graphql-4

 内联⽚段⽤于获取 interface、union 类型的字段。

对象类型

postman-graphql-5

__typename 可以获取结果对象的类型,在不确定返回对象的类型时,可以通过 这种⽅式获取到类型并处理结果。

二、问题

GraphQL Java专注于按照GraphQL规范执行查询。

但是,在数据权限控制、参数校验、分页、JSON、缓存、数据库访问等⽅⾯,并没有提供⽅案, 基本上都需要自实现。

GraphQL也提供了⼀些建议: 如将上下⽂信息写⼊query的执⾏中,在DataFetcher阶段通过DataFetchingEnvironment 获取到上下文信息。

Context context = contextProvider.newContext();
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
 .query(query)
 .variables(variables)
 .operationName(operationName)
 .context(context)
 .build();

再⽐如给query添加唯⼀标识符辅助cache。

在RESTful项⽬中,通过HTTP缓存就可以轻松避免重新获取资源, 客户端通过URL唯⼀标识资源进⾏cache。

而GraphQL希望客户端通过query的唯⼀标识去cache, 或许这也是标量类型GraphQLID存在的原因。

{
 starship(id:"3003") {
     id
     name
 }

 droid(id:"2001") {
     id
     name
     friends {
         id
         name
     }
 }
}

三、小结

GraphQL是为API而生的查询语言,具有需求驱动、一次请求大量资源、兼容性的优点。

GraphQL的三要素:Schema、DataFetcher、Query&Mutation。

GraphQL-Java并没有较为一站式的Web解决方案,需要自行处理比如权限控制、数据库访问、参数校验、cache等问题。