為什麼graphql可以替代restful api?

引言

為什麼graphql可以替代restful api?

上圖是筆者剛參加工作的時候使用的架構圖,當時前後端並未分離,所有代碼都剛在一個工程裡,如果後端代碼改了,前端代碼得跟著改,每次改一點,都需要觸動全身,非常痛苦;

前後端架構

後來隨著公司壯大之後,項目功能也越來越多,改動量也越來越大,實在受不了這種低效率的開發模式了,後來改造成前後端分離的架構,說到前後端分離,它主要分為半分離和完全分離兩種類型。

前後端半分離

為什麼graphql可以替代restful api?

上圖是前後端半分離的架構,為什麼說是半分離,因為不是所有頁面都是單頁面應用,在多頁面應用的情況下,前端因為沒有掌握controller層,前端需要跟後端討論,我們這個頁面是要同步輸出呢,還是異步json渲染呢?因此,在這一階段,只能算半分離。

前後端完全分離

為什麼graphql可以替代restful api?

上圖是完全分離的前後端架構,在這一時期,擴展了前端的範圍。認為controller層也屬於前端的一部分。在這一時期 前端:負責View和Controller層。 後端:只負責Model層,業務處理/數據等。 可是前端不懂後臺代碼呀?controller層如何實現呢? 這就是node.js的妙用了,node.js適合運用在高併發、I/O密集、少量業務邏輯的場景。最重要的一點是,前端不用再學一門其他的語言了,對前端來說,上手度大大提高。

restful 簡介

說到restful,很多人第一反應是“啊,你說的restful就是那個前後端分離的api嗎”,在RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與數據庫的表格名對應。一般來說,數據庫中的表都是同種記錄的"集合"(collection),所以API中的名詞也應該使用複數。

舉例來說,有一個API提供動物園(zoo)的信息,還包括各種動物和僱員的信息,則它的路徑應該設計成下面這樣。

1、https://api.example.com/v1/zoos
2、https://api.example.com/v1/animals
3、https://api.example.com/v1/employees

對於資源的具體操作類型,由HTTP動詞表示。 常用的HTTP動詞有下面五個(括號裡是對應的SQL命令)。

1、GET(SELECT):從服務器取出資源(一項或多項)。
2、POST(CREATE):在服務器新建一個資源。
3、PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
4、PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
5、DELETE(DELETE):從服務器刪除資源。

比如說我要獲取一筆orderid為5的訂單詳情,通常我們都會這樣設計;

GET /orders/5

創建一筆新訂單,我們一般會這樣設計:

POST /orders

restful 缺陷

前端程序員小紅希望在 某個restful api接口中新增一個返回值,後端程序員小明由於忙著談戀愛,沒有時間排期增加;這是restful 第一個缺點,前端請求某個接口,希望新增字段的時候,增加了額外的溝通成本;

過了幾天,前端程序員小紅髮現,現有接口無緣無故多返回了幾個她不需要的屬性,由於web,andorid,ios三個客戶端請求的都是同一個接口,因此小明把這三個客戶端所有的返回值都返回了。而這些額外的字段,小紅並不需要,而且也增加了小紅解析返回值的成本。這是restful的第二個缺點,接口的契約只能約定俗成,不能動態按需的請求字段。

graphql 簡介

GraphQL 是一個用於 API 的查詢語言,是一個使用基於類型系統來執行查詢的服務端運行時(類型系統由你的數據定義)。GraphQL 並沒有和任何特定數據庫或者存儲引擎綁定,而是依靠你現有的代碼和數據支撐。

這是官網的一段簡介,可能聽起來有點矇蔽;簡單的說就是之前restful設計的接口,都是由後端控制的,後端接口一旦確定了,就很難更改,因為是很多客戶端都使用這個接口,所以無法按照某個特定的客戶端去做定製,而且那樣成本也是特別高。但是使用graphql就不一樣了,數據的主動權掌握在客戶端這裡,客戶端想要什麼樣的數據,自己直接去請求就好了,無須服務端去做改動。

一個 GraphQL 服務是通過定義類型和類型上的字段來創建的,然後給每個類型上的每個字段提供解析函數。例如,一個 GraphQL 服務告訴我們當前登錄用戶是 me,這個用戶的名稱可能像這樣:

type Query {
me: User
}
type User {
id: ID
name: String
}

一併的還有每個類型上字段的解析函數:

function Query_me(request) {
return request.auth.user;
}
function User_name(user) {
return user.getName();
}

一旦一個 GraphQL 服務運行起來(通常在 web 服務的一個 URL 上),它就能接收 GraphQL 查詢,並驗證和執行。接收到的查詢首先會被檢查確保它只引用了已定義的類型和字段,然後運行指定的解析函數來生成結果。

例如這個查詢:

{
me {
name
}
}

會產生這樣的JSON結果:

{
"me": {
"name": "Luke Skywalker"
}
}

這個時候,前端小紅希望在要一個age屬性,那就可以這樣寫:

{
me {
name
age
}
}

服務端產生的json結果如下:

{
"me": {
"name": "Luke Skywalker",
"age":12
}
}

你看,想要什麼字段就在查詢語句裡增加字段即可,也不用和後端程序員溝通,而後端程序員也可以愉快的談戀愛了。

Java 操作 graphql

spring-boot 是基於 2.0.0版本,首先加入以下依賴:

<dependency> <groupid>com.graphql-java/<groupid> <artifactid>graphql-spring-boot-starter/<artifactid> <version>4.0.0/<version> /<dependency> <dependency> <groupid>com.graphql-java/<groupid> <artifactid>graphql-java-tools/<artifactid> <version>4.3.0/<version> /<dependency> 下面是graphql服務的入口定義:

type Query { bookById(id: ID): Book } type Book { id: ID name: String pageCount: Int author: Author } type Author { id: ID firstName: String lastName: String } graphql 入口定義了, 但這只是一個描述, 我們需要實現 query中的描述, 在實際開發中,數據的查詢應該從db中獲取,這裡為了方便直接在程序中寫死了。

@Component
public class GraphQLDataFetchers {
private static List> 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> 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");
// TODO 去數據庫查數據

return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
return dataFetchingEnvironment -> {
Map<string> book = dataFetchingEnvironment.getSource();
String authorId = book.get("authorId");
// TODO 去數據庫查數據
return authors
.stream()
.filter(author -> author.get("id").equals(authorId))
.findFirst()
.orElse(null);
};
}
}
/<string>

定義GraphQL:

@Component
public class GraphQLProvider {
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
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(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")

.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
@Bean
public GraphQL graphQL() {
return graphQL;
}
}

定義Controller:

@RestController
@RequestMapping("/test")
public class DemoController {
@Autowired
private GraphQL graphQL;
@GetMapping
public Object find(@RequestParam("query") String query){
ExecutionResult result = graphQL.execute(query);
return result.getData();
}
}

打開postman,使用get請求,請求如下:

http://localhost:8080/test?query={
\tbookById(id:"book-1") {
\t id
\t}
}

返回結果如下:

{
"bookById": {
"id": "book-1"
}
}

這個時候,我需要多返回一個name,請求如下:

http://localhost:8080/test?query={ 

\tbookById(id:"book-1") {
\t id
\t\tname
\t}
}

返回結果如下:

 {
"bookById": {
"id": "book-1",
"name": "Harry Potter and the Philosopher's Stone"
}
}

示例很簡單,但graphql確實很好地解決了不同接口對查詢字段差異性的要求,而不會產生數據冗餘,更多功能還待研究。

當然你會說使用graphql會不會產生額外的工作量? 當然會增加額外的工作量,前期你需要熟悉graphql的語法,後端需要大量的工作;但是這些額外的工作只是在前期,後期的好處就會體現出來了。

總結

本文介紹了傳統的web開發架構的弊端,以及隨著業務的增長,如何改造成前後端分離的架構,進而引申出restful 架構,隨著restful 架構的缺陷,才有後來的代替者graphql。


分享到:


相關文章: