一、GraphQL简介
1.GraphQL是什么?
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
GraphQL的出现不仅仅是针对开发人员的,Facebook在2012年开始在其native mobile apps中使用GraphQL。但有趣的是GraphQL大部分都是在web技术的背景下使用的,并且在native mobile 领域中只得到很少的支持。 Facebook第一次公开谈论GraphQL是在宣布开源计划后不久的2015年React峰会的时候。因为Facebook总是在React的背景下谈GraphQL,所以对于没有React经验的开发人员来说,要理解GraphQL并不是一种仅限于React使用的技术可能还需要一段时间。即便是在这样的背景下诞生的GraphQL依然是一个快速增长的社区 ,事实上GraphQL是一种技术,可以在客户端与API通信的任何地方使用。有趣的是Netflix和Coursera等其他公司都在研究类似的想法以提高API的交互效率。Coursera设想了一种类似的技术,可以让客户指定其数据需求,而Netflix甚至将其解决方案称为Falcor。在GraphQL被开源之后,Coursera完全停止了他们在Falcor上的努力,并转到了GraphQL的学习上。目前已经有很多的公司在使用GraphQL(https://graphql.org/users/)
2.GraphQL产生的历史背景
当提起API设计的时候,大家通常会想到SOAP,RESTful等设计方式,从2000年RESTful的理论被提出的时候,在业界引起了很大反响,因为这种设计理念更易于用户的使用,所以便很快的被大家所接受。我们知道REST是一种从服务器公开数据的流行方式。当REST的概念被提及出来时,客户端应用程序对数据的需求相对简单,而开发的速度并没有达到今天的水平。因此REST对于许多应用程序来说是非常适合的。然而在业务越发复杂,客户对系统的扩展性有了更高的要求时,API环境发生了巨大的变化。特别是从下面三个方面在挑战api设计的方式:
《1.》移动端用户的爆发式增长需要更高效的数据加载
Facebook开发GraphQL的最初原因是移动用户的增加、低功耗设备和松散的网络。GraphQL最小化了需要网络传输的数据量,从而极大地改善了在这些条件下运行的应用程序。
《2.》各种不同的前端框架和平台
前端框架和平台运行客户端应用程序的异构环境使得我们在构建和维护一个符合所有需求的API变得困难,使用GraphQL每个客户机都可以精确地访问它需要的数据。
《3.》在不同前端框架,不同平台下想要加快产品快速开发变的越来越难
持续部署已经成为许多公司的标准,快速的迭代和频繁的产品更新是必不可少的。对于REST api,服务器公开数据的方式常常需要修改,以满足客户端的特定需求和设计更改。这阻碍了快速开发实践和产品迭代。
3.场景举例
《1.》使用REST API来现实时,我们通常可以通过访问多次请求来收集数据。
比如在这个示例中,我们可以通过下面的三步来实现:
1. 通过 /user/<id>获取初始用户数据
2. 通过/user/<id>/posts 返回用户的所有帖子
3. 请求/user/<id>/followers,返回每个用户的关注者列表
![](https://i-blog.csdnimg.cn/blog_migrate/b0415aaebf45cc4b2dce74bd2206d03f.jpeg)
《2.》GraphQL的做法
二、使用GraphQL
具体语法可以先看一遍中文官方文档:http://graphql.cn/
优点:
1.客户端可以自定义查询语句,通过自定义不仅提高了灵活性,而且服务端只返回客户端所需要的数据,减少网络的开销,提高了性能;
2.服务端收到客户端的请求,首先做类型检查,及时反馈,而且更加安全;
3.自动生成文档,降低维护成本;服务端通过新增字段,Deprecated字段,避免版本的繁杂和紊乱。
具体的语法规则可以参考中文文档,下面,我们通过一个springboot项目的例子,看一下怎么使用它?!
三、Springboot中使用
1.配置
《1.》建立springboot项目
参考:https://my.oschina.net/hanchao/blog/1803295
《2.》引入Jar包
<!-- graphQL begin-->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<!-- graphQL end -->
<!-- MYSQL&JPA begin -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<!-- MYSQL&JPA end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<properties>
<java.version>1.8</java.version>
<mysql-connector-java.version>5.1.46</mysql-connector-java.version>
<hikaricp.version>2.4.7</hikaricp.version>
</properties>
注意事项:你的springboot版本不同,引入的GraphQL的版本也会不同!比如:上面我用的是springboot2.x ,我的GraphQL的版本是5.0.2。
如果你的springboot项目为1.5.x,那么你可以引用下面的版本GraphQL。
<!--graphQL-->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
《3.》建立测试表
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `test`;
/*Table structure for table `author` */
DROP TABLE IF EXISTS `author`;
CREATE TABLE `author` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`first_name` varchar(50) DEFAULT NULL COMMENT 'firstName',
`last_name` varchar(50) DEFAULT NULL COMMENT 'lastName',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/*Table structure for table `book` */
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`title` varchar(50) DEFAULT NULL COMMENT '标题',
`author_id` bigint(20) NOT NULL,
`isbn` varchar(255) DEFAULT NULL,
`page_count` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
《4.》建立Entity
BaseEntity.java
package com.hanchao.graphql.graphql.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
/**
* @author hanliwei
* @create 2019-02-12 17:18
*/
@Data
@MappedSuperclass
public class BaseEntity implements Serializable {
/** ID */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "bigint", nullable = false)
protected Long id;
/** 创建时间戳 (单位:秒) */
@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
protected Date createdTime;
/** 更新时间戳 (单位:秒) */
@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
protected Date updatedTime;
public BaseEntity() {
createdTime = new Date();
updatedTime = createdTime;
}
@PreUpdate
private void doPreUpdate() {
updatedTime = new Date();
}
}
Author.java
package com.hanchao.graphql.graphql.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
public class Author extends BaseEntity {
@Column(columnDefinition = "varchar(50)")
private String firstName;
@Column(columnDefinition = "varchar(50)")
private String lastName;
}
Book.java
package com.hanchao.graphql.graphql.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
public class Book extends BaseEntity {
@Column(columnDefinition = "varchar(50)")
private String title;
private String isbn;
private int pageCount;
private long authorId;
}
BookInput.java 参数实体(后面会用到)
package com.hanchao.graphql.graphql.model;
import lombok.Data;
@Data
public class BookInput {
private String title;
private String isbn;
private int pageCount;
private long authorId;
}
《5.》mysql配置
application.properties
############ mysql-开始 ###########
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useServerPrepStmts=true&jdbcCompliantTruncation=false
jdbc.username=root
jdbc.password=root
############ mysql-结束 ###########
DataSourceConfig.java
package com.hanchao.graphql.graphql.config.mysql;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
/**
* @author hanliwei
* @create 2019-01-15 15:07
*/
@Configuration
public class DataSourceConfig {
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName(env.getProperty("jdbc.driver"));
config.setJdbcUrl(env.getProperty("jdbc.url"));
config.setUsername(env.getProperty("jdbc.username"));
config.setPassword(env.getProperty("jdbc.password"));
/**
* 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
*/
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
/**
* 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒
*/
config.setConnectionTimeout(5 * 1000L);
/**
* 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
*/
config.setIdleTimeout(3 * 60 * 1000L);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.setConnectionTestQuery("SELECT 1");
config.setPoolName("beehive_statistic");
return new HikariDataSource(config);
}
}
《6.》Repo
AuthorRepo.java
package com.hanchao.graphql.graphql.repo;
import com.hanchao.graphql.graphql.entity.Author;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author hanliwei
* @create 2019-02-12 17:29
*/
public interface AuthorRepo extends JpaRepository<Author,Long> {
Author findAuthorById(Long id);
}
BookRepo.java
package com.hanchao.graphql.graphql.repo;
import com.hanchao.graphql.graphql.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* @author hanliwei
* @create 2019-02-12 17:31
*/
public interface BookRepo extends JpaRepository<Book,Long> {
List<Book> findByAuthorId(Long id);
Book findBookById(Long id);
}
《7.》Resolver
AuthorResolver.java 用于解析字段
package com.hanchao.graphql.graphql.resolver;
import com.coxautodev.graphql.tools.GraphQLResolver;
import com.hanchao.graphql.graphql.entity.Author;
import com.hanchao.graphql.graphql.entity.Book;
import com.hanchao.graphql.graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.List;
/**
* @author hanliwei
* @create 2019-02-12 17:33
*/
@Component
@AllArgsConstructor
public class AuthorResolver implements GraphQLResolver<Author> {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private BookRepo bookRepo;
public String getCreatedTime(Author author) {
return sdf.format(author.getCreatedTime());
}
public List<Book> getBooks(Author author) {
return bookRepo.findByAuthorId(author.getId());
}
}
BookResolver.java
package com.hanchao.graphql.graphql.resolver;
import com.coxautodev.graphql.tools.GraphQLResolver;
import com.hanchao.graphql.graphql.entity.Author;
import com.hanchao.graphql.graphql.entity.Book;
import com.hanchao.graphql.graphql.repo.AuthorRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author hanliwei
* @create 2019-02-12 17:36
*/
@Component
@AllArgsConstructor
public class BookResolver implements GraphQLResolver<Book> {
private AuthorRepo authorRepo;
public Author getAuthor(Book book) {
return authorRepo.findAuthorById(book.getAuthorId());
}
}
Mutation.java
和root.graphqls文件的方法对应,用于更新操作
package com.hanchao.graphql.graphql.resolver;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.hanchao.graphql.graphql.entity.Author;
import com.hanchao.graphql.graphql.entity.Book;
import com.hanchao.graphql.graphql.model.BookInput;
import com.hanchao.graphql.graphql.repo.AuthorRepo;
import com.hanchao.graphql.graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author hanliwei
* @create 2019-02-12 18:58
*/
@Component
@AllArgsConstructor
public class Mutation implements GraphQLMutationResolver {
private AuthorRepo authorRepo;
private BookRepo bookRepo;
public Author newAuthor(String firstName, String lastName) {
Author author = new Author();
author.setFirstName(firstName);
author.setLastName(lastName);
return authorRepo.save(author);
}
public Book newBook(String title, String isbn, int pageCount, Long authorId) {
Book book = new Book();
book.setTitle(title);
book.setIsbn(isbn);
book.setPageCount(pageCount);
book.setAuthorId(authorId);
return bookRepo.save(book);
}
public Book saveBook(BookInput input) {
Book book = new Book();
book.setTitle(input.getTitle());
book.setIsbn(input.getIsbn());
book.setPageCount(input.getPageCount());
book.setAuthorId(input.getAuthorId());
return bookRepo.save(book);
}
public Boolean deleteBook(Long id) {
bookRepo.deleteById(id);
return true;
}
public Book updateBookPageCount(int pageCount,long id) {
Book book = bookRepo.findBookById(id);
book.setPageCount(pageCount);
return bookRepo.save(book);
}
}
Query.java
和root.graphqls中的方法对应!用于查询操作
package com.hanchao.graphql.graphql.resolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.hanchao.graphql.graphql.entity.Author;
import com.hanchao.graphql.graphql.entity.Book;
import com.hanchao.graphql.graphql.repo.AuthorRepo;
import com.hanchao.graphql.graphql.repo.BookRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author hanliwei
* @create 2019-02-12 18:00
*/
@Component
@AllArgsConstructor
public class Query implements GraphQLQueryResolver {
private AuthorRepo authorRepo;
private BookRepo bookRepo;
public Author findAuthorById(Long id) {
return authorRepo.findAuthorById(id);
}
public List<Author> findAllAuthors() {
return authorRepo.findAll();
}
public Long countAuthors() {
return authorRepo.count();
}
public List<Book> findAllBooks() {
return bookRepo.findAll();
}
public Long countBooks() {
return bookRepo.count();
}
}
《8.》建立graphql文件
在resource下面建立一个文件夹graphql ,该文件夹下面必须有两个文件,
名字分别为:root.graphqls
schema.graphqls
root.graphqls
#查询入口
type Query {
####### Author相关 #########
findAuthorById(id:Long!): Author
findAllAuthors: [Author]!
countAuthors: Long!
####### Book相关 #########
findAllBooks: [Book]!
countBooks: Long!
}
type Mutation {
newAuthor(firstName: String!,lastName: String!) : Author!
newBook(title: String!,isbn: String!,pageCount: Int, authorId: Long!) : Book!
saveBook(input: BookInput!) : Book!
deleteBook(id: ID!) : Boolean
updateBookPageCount(pageCount: Int!, id:Long!) : Book!
}
schema.graphqls
注意:我们需要给每个字段加上注释,要不使用者会很崩溃!
type Author {
#作者Id
id: Long!
#创建时间
createdTime: String
#名
firstName: String
#姓
lastName: String
#该作者的所有书籍
books: [Book]
}
input BookInput {
title: String!
isbn: String!
pageCount: Int
authorId: Long
}
type Book {
id: Long!
title: String!
isbn: String!
pageCount: Int
author: Author
}
加了注释后的效果:
注意事项:如果都放在schema.graphqls中也可以,可以类似这样
《9.》测试
到此,项目应该建立好了,可以启动了,访问:http://localhost:8080/graphiql ,可以测试一下root.graphqls中的方法了!
具体测试过程,我们放在下一篇文章中吧,要不感觉一篇博文太长了!
源码:https://github.com/windhan2100/graphql