记录初学Spring boot中使用GraphQL编写API的几种方式

Spring boot+graphql

一、使用graphql-java-tools方式

<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.6.0</version>
</dependency>

<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.4</version>
</dependency>

schema.graphqls

type Query {
    books: [Book!]
}

type Book {
    id: Int!
    name: String!
    author: Author!
}

type Author {
    id: Int!
    name: String!
}

对应的java class

class Book {
    private int id;
    private String name;
    private int authorId;

    // constructor

    // getId
    // getName
    // getAuthorId
}

class Author {
    private int id;
    private String name;

    // constructor

    // getId
    // getName
}

Book-Resolver

class BookResolver implements GraphQLResolver<Book> {

    private AuthorRepository authorRepository;

    public BookResolver(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    public Author author(Book book) {
        return authorRepository.findById(book.getAuthorId());
    }
}

Query-Resolver

class Query implements GraphQLQueryResolver {

    private BookRepository bookRepository;

    public Query(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public List<Book> books() {
        return bookRepository.findAll();
    }
}

Type Query 没有对应的java class,如果type 中的所有字段和java class成员变量一致,则该type可以不用定义Resolver.

graphql type中的字段映射Java class字段的优先级

对于graphql objectType中的字段映射为java class字段顺序如下:

  1. method (*fieldArgs [, DataFetchingEnvironment])
  2. method is(*fieldArgs [, DataFetchingEnvironment]), only if the field returns a Boolean
  3. method get(*fieldArgs [, DataFetchingEnvironment])
  4. method getField(*fieldArgs [, DataFetchingEnvironment])
  5. field

例如:

上述type Book中的name字段的映射顺序为:

  1. 在java Book类中找name(参数)方法。
  2. 如果Book类中没有name(参数)方法,则继续找isName(参数)方法。
  3. 如果Book中没有isName(参数)方法,则继续在Book中找getName(参数)方法。
  4. 如果Book中没有getName()方法,则继续在Book中找getFieldName(参数)方法。
  5. 如果Book中没有getFieldName(参数)方法,在继续在Book中找name成员变量。
  6. 如果Book中没有name成员变量,则报错。

graphql type中的字段映射Resolver的优先级:

  1. method (dataClassInstance, *fieldArgs [, DataFetchingEnvironment])
  2. method is(dataClassInstance, *fieldArgs [, DataFetchingEnvironment]), only if the field returns a Boolean
  3. method get(dataClassInstance, *fieldArgs [, DataFetchingEnvironment])
  4. method getField(dataClassInstance, *fieldArgs [, DataFetchingEnvironment])

注:Resolver的映射优先级高于Java Class,首先在Resolver中查找,如果没找到,才会在Java class中查找 :例如上述type Book中的author字段,会首先映射为BookResolver重的author(Book)方法。

解析schema.graphqls,创建Graphql对象:

import com.coxautodev.graphql.tools.SchemaParser;

GraphQLSchema schema = SchemaParser.newParser().file("schema.graphqls")
        .resolvers(new QueryResolver(), new BookResolver())
        .build()
        .makeExecutableSchema();

GraphQL graphQL = GraphQL.newGraphQL(schema).build();
// 执行查询
ExecutionResult result = graphQL.execute(query);

Map<String, Object> map = result.toSpecification();

二、不使用Resolver

schema.graphqls:

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

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

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

加载schema.graphqls,创建GraphQL对象:

import graphql.schema.idl.SchemaParser;

@Value("classpath:schema.graphqls")
Resource resource;

@PostConstruct
private void loadSchema() throws Exception {
    File schemaFile = resource.getFile();

    GraphQLSchema schema = buildSchema(schemaFile);

    graphQL = GraphQL.newGraphQL(schema).build();
}

private GraphQLSchema buildSchema(File file) throws Exception {
    TypeDefinitionRegistry registry = new SchemaParser().parse(file);
    RuntimeWiring runtimeWiring = buildWiring();
    SchemaGenerator schemaGenerator = new SchemaGenerator();
    return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}

private RuntimeWiring buildWiring() {
    return RuntimeWiring.newRuntimeWiring()
        // 为每个graphql type的字段提供DataFetcher
        .type(newTypeWiring("Query")
              .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
        .type(newTypeWiring("Book")
              .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
        .build();
}

DataFetcher:

@Component
public class GraphQLDataFetchers {

    private static List<Map<String, String>> books = Arrays.asList(
            ImmutableMap.of("id", "book-1",
                    "name", "Harry Potter and the Philosopher's Stone",
                    "pageCount", "223",
                    "authorId", "author-1"),
            ImmutableMap.of("id", "book-2",
                    "name", "Moby Dick",
                    "pageCount", "635",
                    "authorId", "author-2"),
            ImmutableMap.of("id", "book-3",
                    "name", "Interview with the vampire",
                    "pageCount", "371",
                    "authorId", "author-3")
    );

    private static List<Map<String, String>> authors = Arrays.asList(
            ImmutableMap.of("id", "author-1",
                    "firstName", "Joanne",
                    "lastName", "Rowling"),
            ImmutableMap.of("id", "author-2",
                    "firstName", "Herman",
                    "lastName", "Melville"),
            ImmutableMap.of("id", "author-3",
                    "firstName", "Anne",
                    "lastName", "Rice")
    );

    public DataFetcher getBookByIdDataFetcher() {
        return dataFetchingEnvironment -> {
            String bookId = dataFetchingEnvironment.getArgument("id");
            return books
                    .stream()
                    .filter(book -> book.get("id").equals(bookId))
                    .findFirst()
                    .orElse(null);
        };
    }

    public DataFetcher getAuthorDataFetcher() {
        return dataFetchingEnvironment -> {
            Map<String,String> book = dataFetchingEnvironment.getSource();
            String authorId = book.get("authorId");
            return authors
                    .stream()
                    .filter(author -> author.get("id").equals(authorId))
                    .findFirst()
                    .orElse(null);
        };
    }
}

注:这种方式,并不要求一定要提供type对应的java class,只要在对应的DataFetcher中返回符合type的数据格式即可

三、方式三,不使用graphql-java-tools

不定义schema.graphqls,以编码的方式创建graphql type。

定义graphql type:

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

上述代码相当于使用schema方式创建了如下graphql type:

type Foo {
    bar: String
}

为类型的field指定DataFetcher:

DataFetcher<Foo> fooDataFetcher = environment -> {
    // environment.getSource() is the value of the surrounding
    // object. In this case described by objectType
    Foo value = perhapsFromDatabase(); // Perhaps getting from a DB or whatever
    return value;
}

GraphQLObjectType objectType = newObject()
    .name("ObjectType")
    .field(newFieldDefinition()
           .name("foo")
           .type(GraphQLString)
           .dataFetcher(fooDataFetcher))
    .build();

完整的代码:

// 定义一个type Query 
/**
*  相当于 type QueryType{ hello: String }
*/
GraphQLObjectType queryType = newObject()
            .name("QueryType")
            .field(newFieldDefinition()
                    .name("hello")
                    .type(GraphQLString)
                    .dataFetcher(new StaticDataFetcher("world!"))
            .build();
// 创建GraphQLSchema
    GraphQLSchema schema = GraphQLSchema.newSchema()
            .query(queryType)
            .build();

    // Make the schema executable
    GraphQL executor = GraphQL.newGraphQL(graphQLSchema).build();
    ExecutionResult executionResult = executor.execute("{hello}");

四、参考链接

https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/

https://www.graphql-java-kickstart.com/tools/

https://www.graphql-java.com/documentation/v11/

上一篇:GraphQL 在 HTTP/2 世界中仍然有意义吗?


下一篇:GraphQL简介