1. 程式人生 > 程式設計 >開始使用 GraphQL 和 Spring Boot

開始使用 GraphQL 和 Spring Boot

1. 介紹

GraphQL是一個來自於 Facebook 的相當新的概念,它讓我們在寫 Web API 的時候作為 REST 介面風格的另一種選擇。 這篇文章將會介紹如何通過 Spring Boot 來搭建我們的 GraphQL 服務,這樣無論在現有專案或者新專案裡都可以很方便地使用。

2. 什麼是 GraphQL ?

傳統的 REST API 是依據伺服器管理資源的概念來編寫的。這些資源可以通過 Http 請求以規定的幾個 verb(GET、POST、PUT、DELETE) 來進行訪問。當我們的介面和我們的資源概念相符合時工作沒什麼問題,但如果我們稍有變化,事情就開始變的麻煩了。

這同時還會發生在我們的客戶端請求多個不同資料的時候:比方說我們請求部落格的文章以及對應的評論。通常我們只能讓客戶端發多個請求,或者讓服務端在同一個介面裡提供這些額外的資料,但這些資料並不總是需要的,這就違背了 REST 的設計,同時還導致了服務端的響應包體變大,造成網路傳輸的浪費。

GraphQL 提供了一個能夠同時解決這兩個問題的方案. 它允許客戶端在一個請求裡指明所需要的資料,還可以實現在一個請求裡傳送多個查詢。

它工作起來更像是 RPC 服務,它不用固定的 verb,而是使用 命名查詢(Query)命名修改(Mutation) 的方式。這就讓業務編寫的控制權回到了它應有的地方,API 介面的開發者來確定哪些介面行為是被允許的, API 介面的使用者在執行時動態指明他們想要什麼資料

舉個例子,一個 blog 可能會有如下的查詢請求:

query {
    recentPosts(count: 10,offset: 0) {
        id
        title
        category
        author {
            id
            name
            thumbnail
        }
    }
}
複製程式碼

這個查詢請求將會:

  • 請求10篇最新的文章
  • 每一篇文章會返回 ID,title,category 欄位
  • 對於每一篇文章還會返回作者,其中作者資訊裡包含了id,name,thumbnail

在傳統的 REST API 裡,這要麼需要傳送11個請求 —— 1個介面用來請求文章列表,另10個介面請求相對應的作者。或者需要在服務端 /posts 的介面裡把作者的資訊都包含進去。

2.1. GraphQL Schemas

GraphQL 服務會提供一個 schema 來完整描述所有的 API 介面。這個 schema 檔案包含了具體的資料型別定義(type)。每個資料型別會有一個或多個欄位(field),每個欄位會有0到多個引數(parameter),以及對應的返回型別(type)。

通過這些欄位的巢狀組合形成了一個圖資料結構(也就是 GraphQL 裡 Graph 的含義)。整個圖不需要避免環,出現環也是完全可以接受的,但一定是有向圖。也就是說,客戶端可以通過一個型別節點的欄位找到它的子節點,但是無法通過子節點直接反向找到父親節點(除非在 schema 裡單獨定義出來)。

舉剛才 blog 的例子,它包含了如下的型別定義:一個 Post 結構,Post 裡 author 欄位對應的 Author結構,以及從根查詢(Root Query)節點上通過 recentPosts 欄位來找出最新的 Post

type Post {
    id: ID!
    title: String!
    text: String!
    category: String
    author: Author!
}
 
type Author {
    id: ID!
    name: String!
    thumbnail: String
    posts: [Post]!
}
 
# 整個應用的根查詢(讀操作),它也是一個型別
type Query {
    recentPosts(count: Int,offset: Int): [Post]!
}
 
# 整個應用的根修改 (寫操作)
type Mutation {
    writePost(title: String!,text: String!,category: String) : Post!
}
複製程式碼

一些欄位型別後面帶有 "!" 意味著這個欄位是非空的,如果沒有的話就說明是可選的。在我們請求介面的時候,如果對應的可選欄位伺服器返回了空物件,GraphQL也能夠正確處理後續的查詢,比方說 recentPosts 介面裡幾篇文章的 category 是空。

GraphQL 服務也通過介面暴露出了 schema,這樣客戶端就可以提前獲取 schema 定義便於處理。這使得當 schema 改變了的時候,客戶端能夠自動發現並動態調整資料結構。一個很有用的場景就是可以使用 GraphiQL 工具(注意中間多了一個 i)來和服務端進行互動(類似於 Postman 等 REST Client)。

3. GraphQL Spring Boot Starter 介紹

Spring Boot GraphQL Starter 提供了一個便捷的方式讓我們快速地執行起一個 GraphQL 服務. 它和 GraphQL Java Tools 配合,讓我們只需要寫很少的程式碼就可以啟動起來。

3.1. 配置服務

我們只需要加入如下的依賴:

<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>
複製程式碼

Spring Boot 就會自動設定好相應的 handler 預設情況下,GraphQL 服務會通過 /graphql 介面暴露出來,通過 POST 該介面就可以傳送對應請求,介面地址可以在 application.properties 裡修改。

3.2. 定義 Schema

GraphQL Tools 可以通過處理 GraphQL Schema 檔案來生成正確的結構物件,並繫結到對應的 bean 物件上。這些schema檔案只要以 “.graphqls” 副檔名結尾並存在於 classpath裡,Spring Boot GraphQL starter 就可以自動找到這些 schema 檔案,所以我們完全可以把這些檔案按模組劃分管理。 但我們只能有一個根查詢,也必須有一個根查詢的定義。而 Mutation 定義可以沒有或者有一個。這個限制是源於 GraphQL Schema 規則,而不是因為 Java 無法實現。

3.3. 根查詢解析器

根查詢需要通過在Spring裡定義特殊的 java bean 物件,從而來處理不同的欄位查詢。它不像 schema 的定義檔案,這些bean物件可以有多個。我們只需要實現 GraphQLQueryResolver 介面,然後在 schema 裡的每個欄位都有相對應名字的屬性或函式就可以了。

public class Query implements GraphQLQueryResolver {
    private PostDao postDao;
    public List<Post> getRecentPosts(int count,int offset) {
        return postsDao.getRecentPosts(count,offset);
    }
}
複製程式碼

這些屬性或函式會按如下的規則順序查詢:

  • <field>
  • is<field> 僅當該欄位是個 Boolean 變數時
  • get<field>

如果schema裡對應欄位有定義引數的話,這些函式也需要按照對應的順序來定義(像 count 與 offset),函式引數最後可以有一個可選的 DataFetchingEnvironment 型別的引數,來獲取一些上下文資訊。 這些函式的返回值也需要和 schema 裡對應起來,一會兒我們就會看到。所有的原生型別 String,Int,List,等等 都可以和相應的 Java 型別對應起來。

像上面的這個 getRecentPosts 方法就會對應上 GraphQL schema 裡 recentPosts 這個查詢欄位。

3.4. 通過 Bean 物件來對映 GraphQL 型別

在 GraphQL 服務裡,無論是在根節點還是任何一個結構裡,所有複雜的型別都可以對應上 Java Bean 物件。每一個 GraphQL 型別只能有一個 Java 類來對應,但 Java 的類名並不一定要和 GraphQL 裡的型別名一樣 Java bean 裡的屬性名會被對映成 GraphQL 返回資料的欄位名

public class Post {
    private String id;
    private String title;
    private String category;
    private String authorId;
}
複製程式碼

在 Java bean 裡的屬性和方法如果在 GraphQL schema 找不到相應定義的都會被直接忽略掉而不會出什麼問題。這個機制可以用來處理一些複雜情況。 舉例來說,這裡的 authorId 在 schema 裡並沒有任何的定,所以在介面裡就不會出現,但它可以在接下來的步驟裡使用:

3.5. 複雜物件的欄位解析

有時候,一個欄位的資料並不能直接訪問,它有可能涉及到資料庫查詢,複雜的計算,或者一些別的什麼情況。 GraphQL Tools 有一套機制來處理這些場景 它可以用 Spring 的 Bean 物件來為這些普通 Bean 提供資料。

我們通過使用普通 Bean 名字後面加上 Resolver ,然後再實現 GraphQLResolver 介面,就可以使用 Spring Bean 來為普通 Bean 提供額外的欄位解析,然後在 Spring Bean裡的方法都需要遵循上面的命名規則,唯一的區別是這些方法的第一個引數會是普通Bean物件。 如果欄位同時存在於普通 Bean 和 Resolver上的話,Resolver上的會被優先採用。

@Repository
public class PostResolver implements GraphQLResolver<Post> {

    @Autowired
    private AuthorDao authorDao;
 
    public Author getAuthor(Post post) {
        return authorDao.getAuthorById(post.getAuthorId());
    }
}
複製程式碼

這些 Resolver 會被 Spring 上下文載入,所以這可以使得我們可以使用很多 Spring 的策略,比方說注入 DAO。

和上面一樣,如果客戶端並沒有請求對應的欄位的話,GraphQL 將不會去獲取相應的資料。這就意味著如果客戶端去獲取了一個 Post 但沒有要請求 author 欄位,那麼 Resolver 裡的 getAuthor() 方法將不會被呼叫,從而相應的 DAO 請求也不會發出。

3.6. 可選值

GraphQL Schema 有 Optional 的概念,一些型別可以是可選為空的,另一些就是非空的。 這在 Java 裡可以直接使用 null 來表示和處理,相應的,如果是在 Java 8 環境下,可以使用 Optional 型別來表示可選,無論是哪種方式,系統都能正確處理。這個機制就讓我們的 GraphQL schema 能和 Java 程式碼更好地對應起來。

3.7. 修改(Mutation)

到現在為止,我們一直在討論從服務端獲取資料,GraphQL 同樣還可以更新服務端的資料,在 GraphQL 裡就是 Mutation。 從程式碼的角度來說,一個 Query 請求沒理由不能直接修改資料,我們可以很容易用 Query Resolver 來接受一些引數然後修改資料,最後返回給客戶端,在這裡採用 Mutation 主要是為了更好地規範。

相應地, 修改(Mutation)介面 應該僅用於告知客戶端該操作會改變服務端儲存資料 在 Java 程式碼裡,我們只需要把 GraphQLQueryResolver 介面換成 GraphQLMutationResolver 就可以定義一個根修改介面,其它的所有規則都和查詢介面一樣,修改介面的返回值就和查詢介面一樣,可以巢狀等。

public class Mutation implements GraphQLMutationResolver {
    private PostDao postDao;
 
    public Post writePost(String title,String text,String category) {
        return postDao.savePost(title,text,category);
    }
}
複製程式碼

4. 有關 GraphiQL

GraphQL 經常會和 GraphiQL 一起使用,GraphiQL 是一個可以直接和 GraphQL 服務互動的 UI 介面,可以執行查詢和修改請求,可以從這裡下載獨立的基於 Electron 的 GraphiQL 應用。 在我們的應用裡還可以直接整合基於 Web 版本的 GraphiQL,我們只需要加入以下依賴

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>
複製程式碼

就可以在 /graphiql 裡看到,但這隻適用於 graphql 介面在 /graphql 的預設情況,如果有調整就還需要獨立客戶端。

5. 小結

GraphQL 是個非常令人激動的新技術,它讓我們開發介面的時候有不一樣的視角,把 Spring Boot GraphQL Starter 和 GraphQL Java Tools 結合起來非常容易,它可以讓我們輕易地加到現有應用裡或者乾脆建立一個新的應用。

我的部落格