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模拟。
一个相对完整的流程就完成了。
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
}
}
}
传递变量
@include @skip
通过指令@include 决定是否包含某些内容,使⽤场景如:预览和详情。
内联片段
内联⽚段⽤于获取 interface、union 类型的字段。
对象类型
__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等问题。