Java中的GraphQL服務器:第二部分:瞭解解析器

第二部分:瞭解解析器html

在第一部分中,咱們開發了一個很是簡單的GraphQL服務器。該解決方案有一個嚴重的缺陷:全部字段都急切地加載到後端,即便前端未要求也是如此。經過不給客戶任何選擇,咱們經過RESTful服務來接受這種狀況。RESTful API始終返回全部內容,這意味着始終加載全部內容。另外一方面,若是將RESTful API分爲多個較小的資源,則可能會面臨N + 1問題和屢次網絡往返的風險。GraphQL是專門爲解決如下問題而設計的:前端

  • 僅獲取必需的數據,以免額外的網絡流量以及後端沒必要要的工做
  • 容許在單個請求中獲取客戶端所需的儘量多的數據,以減小整體延遲

RESTful API能夠任意決定要返回多少數據,所以幾乎沒法解決上述問題。它要麼獲取過多,要麼獲取不足。好的,這是理論,可是咱們對GraphQL服務器的實現不能以這種方式工做。不管是否請求,它仍將獲取全部數據。傷心。java

不斷髮展您的API

回顧一下咱們的API返回一個實例PlayerDTO:spring

@Value
class Player {
    UUID id;
    String name;
    int points;
    ImmutableList<Item> inventory;
    Billing billing;
}

與此GraphQL模式匹配的:後端

type Player {
    id: String!
    name: String!
    points: Int!
    inventory: [Item!]!
    billing: Billing!
}

經過仔細地分析咱們的應用程序,我意識到不多有客戶billing在他們的查詢中提出要求,可是咱們必須始終提出要求billingRepository才能建立Player實例。許多急切的,不須要的工做:安全

private final BillingRepository billingRepository;
private final InventoryClient inventoryClient;
private final PlayerMetadata playerMetadata;
private final PointsCalculator pointsCalculator;

//...

@NotNull
private Player somePlayer() {
    UUID playerId = UUID.randomUUID();
    return new Player(
            playerId,
            playerMetadata.lookupName(playerId),
            pointsCalculator.pointsOf(playerId),
            inventoryClient.loadInventory(playerId),
            billingRepository.forUser(playerId)
    );
}

像這樣的字段billing只能在請求時加載!爲了瞭解如何使咱們的對象的某些部分_圖_(Graph-QL是也)懶加載,讓咱們添加一個名爲新特性trustworthinessPlayer上:服務器

type Player {
    id: String!
    name: String!
    points: Int!
    inventory: [Item!]!
    billing: Billing!
    trustworthiness: Float!
}

此更改是向後兼容的。事實上,GraphQL實際上沒有API版本控制的概念。那麼遷移路徑是什麼?有如下幾種狀況:網絡

  • 您錯誤地將新架構提供給客戶端,而沒有實現服務器。在這種狀況下,客戶端會快速失敗,由於它請求trustworthiness服務器還沒有交付的字段。好。另外一方面,使用RESTful API,客戶端認爲服務器將返回一些數據。這可能會致使意外錯誤或服務器故意返回的假設null(丟失字段)
  • 您添加了trustworthiness字段,但沒有分發新的架構。還行吧。客戶不知道,trustworthiness因此他們不要求它。
  • 服務器準備就緒後,便將新架構分發給客戶端。客戶端可能會或可能不會使用新數據。不要緊。

可是,若是您犯了一個錯誤並向全部客戶端宣佈服務器的新版本支持某些架構而實際上卻不支持該架構,該怎麼辦?換句話說,服務器僞裝爲support trustworthiness,可是在被詢問時它不知道如何計算。這有可能嗎?架構

Caused by: [...]FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:

  com.nurkiewicz.graphql.Player.trustworthiness()
  com.nurkiewicz.graphql.Player.getTrustworthiness()
  com.nurkiewicz.graphql.Player.trustworthiness

這是在服務器啓動時發生的!若是您在不實施底層服務器的狀況下更改了架構,它甚至將沒法啓動!這真是個好消息。若是您宣佈支持某些架構,則不可能發佈不支持該架構的應用程序。改進API時,這是一個安全網。僅當服務器支持架構時,纔將架構交付給客戶端。而且,當服務器宣佈某些架構時,您能夠100%確保其正常工做並正確格式化。響應中沒有其餘缺乏的字段,由於您正在詢問服務器的舊版本。再也不有僞裝支持某些API版本的損壞服務器,而實際上,您忘記了將字段添加到響應對象。dom

用懶加載的Resolver代替貪婪加載值

好了,那麼我該如何添加trustworthiness以遵照新的架構?在_不那麼聰明的_技巧是正確的,在該阻止咱們的應用程序啓動異常。它說它正在嘗試尋找方法,獲取器或字段trustworthiness。若是咱們盲目地將其添加到Player類中,API將會起做用。那是什麼問題 請記住,在更改架構時,舊客戶端不知道trustworthiness。即便知道這一點的新客戶,也可能永遠不會或不多要求它。換句話說,trustworthiness只須要爲一小部分請求計算出值。不幸的是,由於trustworthiness是現場上Player課,咱們必須老是熱切計算。不然,沒法實例化並返回響應對象。有趣的是,使用RESTful API,這一般不是問題。只需加載並返回全部內容,讓客戶決定忽略什麼。可是咱們能夠作得更好。

首先,trustworthinessPlayerDTO中刪除字段。咱們必須更深刻,我是說懶惰。而是,建立如下組件:

import com.coxautodev.graphql.tools.GraphQLResolver;
import org.springframework.stereotype.Component;

@Component
class PlayerResolver implements GraphQLResolver<Player> {

}

保持空白,GraphQL引擎將指導咱們。當嘗試再次運行該應用程序時,該異常很熟悉,但並不相同:

FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:

  com.nurkiewicz.graphql.PlayerResolver.trustworthiness(com.nurkiewicz.graphql.Player)
  com.nurkiewicz.graphql.PlayerResolver.getTrustworthiness(com.nurkiewicz.graphql.Player)
  com.nurkiewicz.graphql.PlayerResolver.trustworthiness
  com.nurkiewicz.graphql.Player.trustworthiness()
  com.nurkiewicz.graphql.Player.getTrustworthiness()
  com.nurkiewicz.graphql.Player.trustworthiness

trustworthiness不只在Player類上,並且還在PlayerResolver咱們剛剛建立的上尋找。您能發現這些簽名之間的區別嗎?

  • PlayerResolver.getTrustworthiness(Player)
  • Player.getTrustworthiness()

前一種方法Player做爲參數,然後一種方法Player自己就是實例方法(getter)。目的是PlayerResolver什麼?默認狀況下,GraphQL模式中的每種類型都使用默認解析器。該解析器基本上採用例如的實例, Player並檢查吸氣劑,方法和字段。可是,您可使用更復雜的默認解析器來裝飾該默認解析器。一種能夠懶惰地計算給定名稱的字段。尤爲是在Player課堂上沒有這樣的領域時。最重要的是,僅當客戶端實際請求該字段時才調用該解析程序。不然,咱們將退回到默認解析器,該解析器指望全部字段都屬於Player對象自己。那麼,您如何實現自定義解析器trustworthiness呢?該異常將指導您:

@Component
class PlayerResolver implements GraphQLResolver<Player> {

    float trustworthiness(Player player) {
        //slow and painful business logic here...
        return 42;
    }

}

固然,在現實世界中,實現會作一些聰明的事情。採起Player,應用一些業務邏輯,等等。真正重要的是,若是客戶不想知道trustworthiness,永遠不會調用此方法。懶!經過添加一些日誌或指標來本身查看。是的,指標!這種方法還使您能夠深刻了解本身的API。客戶很是明確,只詢問必填字段。所以,您能夠爲每一個解析器獲取指標,並快速找出哪些字段已使用,哪些字段無效以及能夠棄用或刪除。另外,您能夠輕鬆地發現哪一個特定字段的加載成本很高。使用RESTful API的全有或全無的方法,這種細粒度的控制是不可能的。爲了使用RESTful API停用字段,您必須建立資源的新版本並鼓勵全部客戶端遷移。

懶惰的一切

若是您想變得更加懶惰並消耗盡量少的資源,則Player能夠將的每一個單個字段委派給解析器。模式保持不變,但Player類變爲空心:

@Value
class Player {
    UUID id;
}

那麼,如何GraphQL知道如何計算namepointsinventorybillingtrustworthiness?好吧,解析器上有一個方法能夠解決如下每一個問題:

@Component
class PlayerResolver implements GraphQLResolver<Player> {

    String name(Player player) {
        //...
    }

    int points(Player player) {
        //...
    }

    ImmutableList<Item> inventory(Player player) {
        //...
    }

    Billing billing(Player player) {
        //...
    }

    float trustworthiness(Player player) {
        //...
    }

}

實現並不重要。重要的是惰性:只有在請求某些字段時才調用這些方法。這些方法中的每個均可以分別進行監視,優化和測試。從性能角度來看,這很棒。

性能問題

您是否注意到inventorybilling字段互不相關?即,獲取inventory可能須要調用某些下游服務,而billing須要SQL查詢。不幸的是,GraphQL引擎將響應組合成一個順序。咱們將在下一期中解決此問題,敬請期待!

相關文章
相關標籤/搜索