在設計GraphQL架構時,有一些常見的問題需要注意,以確保系統的效能和安全性。以下列出了一些須留意的GraphQL問題:
在GraphQL中,N+1查詢問題是一個常見的效能問題,它發生在server為每個項目的列表中的每個項目執行單獨的查詢時。解決這個問題的一種方法是使用dataloader來批量處理查詢,減少查詢次數。
Prisma有一篇關於介紹解決n+1問題的文章。他提供的建議是:
include
或select
降低查詢複雜度。如果model有與其他模型的關聯,我們能考慮使用include
或select
來優化查詢createMany
, deleteMany
, updateMany
, 或 findMany
方法可以提高效能。並且可以在單個操作中處理多條記錄,而不是單獨處理每條記錄GraphQL查詢可能會變得非常複雜,並且可能會深入多個層次。通過限制查詢的複雜度和深度,可以保護服務器免受過度使用和濫用。文章Link
我們先安裝依賴pnpm add graphql-query-complexity apollo-server-plugin-base
首先創建plugin,並且複雜度設為100。相關方法詳見Doc
///// apps\iron-ecommerce-server\src\graphql\gql-complexity.plugin.ts
import { ApolloDriverConfig } from "@nestjs/apollo";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { GqlOptionsFactory } from "@nestjs/graphql";
import { GraphQLError } from "graphql";
import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from "graphql-query-complexity";
import { GraphqlConfig } from "../common/configs/config.interface";
// ....
@Injectable()
export class GqlSchemaFirstConfigService implements GqlOptionsFactory {
constructor(private configService: ConfigService, private gqlSchemaHost: GraphQLSchemaHost) {}
createGqlOptions(): ApolloDriverConfig {
const graphqlConfig = this.configService.get<GraphqlConfig>("graphql");
const host = this.gqlSchemaHost;
return {
typePaths: graphqlConfig.schemafirst.typePaths,
definitions: graphqlConfig.schemafirst.definitions,
playground: graphqlConfig.schemafirst.playgroundEnabled,
plugins: [
{
async requestDidStart() {
const maxComplexity = 100;
const { schema } = host;
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })]
});
if (complexity > maxComplexity) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`
);
}
console.log("Query Complexity:", complexity);
}
};
}
}
]
};
}
}
接著加入complexity在field和query上
///// apps\iron-ecommerce-server\src\api\products\products.model.ts
import { Field, InputType, Int, ObjectType } from "@nestjs/graphql";
import PaginatedResponse from "../../common/pagination/pagination";
@ObjectType()
export class Product {
@Field({ complexity: 1 })
id: string;
@Field({ complexity: 1 })
name: string;
@Field({ complexity: 1 })
price: number;
@Field({ complexity: 1 })
description: string;
@Field({ complexity: 1 })
imageUrl: string;
}
// ...
///// apps\iron-ecommerce-server\src\api\products\products.resolver.ts
import { Args, ComplexityEstimatorArgs, Mutation, Query, Resolver } from "@nestjs/graphql";
import { PaginationArgs } from "../../common/pagination/pagination.args";
import { NewProductInput, Product, ProductConnection, UpdateProductInput } from "./products.model";
import { ProductService } from "./products.service";
@Resolver(() => Product)
export class ProductsResolver {
constructor(private readonly productService: ProductService) {}
@Query(() => ProductConnection, {
complexity: (options: ComplexityEstimatorArgs) => (options.args.first ?? 1) * options.childComplexity
})
async getProducts(
@Args() { after, before, first, last }: PaginationArgs,
@Args({ name: "query", type: () => String, nullable: true }) query?: string
): Promise<ProductConnection> {
return await this.productService.findAll({ first, last, before, after }, query);
}
// ...
接著我們執行query,我們現在能在server端上看到console
{
getProducts(first: 1) {
edges {
cursor
node {
id
name
description
imageUrl
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
totalCount
}
}
///// response
Query Complexity: 14
在GraphQL中,一個重要的特點是即使在發生錯誤時,它也會返回一個200 OK HTTP狀態碼,與REST不同,REST會返回如400或500等特定的錯誤狀態碼。此外,GraphQL允許在單個請求中執行多個操作,因此可能會有多個錯誤。為了有效處理這些錯誤,應該:
- 錯誤分類: 明確區分客戶端錯誤(例如,查詢格式錯誤)和服務器錯誤(例如,資料庫連接失敗),並通過不同的錯誤碼或消息來表示它們。
- 詳細的錯誤信息: 提供足夠的錯誤信息,以便開發人員和系統管理員可以快速識別和解決問題。
在GraphQL中,授權和認證的實施可能會比RESTful API更為複雜,因為GraphQL允許客戶端指定它們想要什麼資料,而RESTful API通常有固定的端點和返回結構。解決方案包括:
- 字段級別的授權: 考慮在字段級別實施授權,以確保只有具有適當權限的用戶可以訪問敏感資料。
- 使用身份驗證中間件: 確保在解析器執行之前驗證用戶的身份。
由於GraphQL查詢的靈活性,它的緩存實施可能會比RESTful API更為複雜。例如,同一個資源可以通過多個不同的查詢獲取,每個查詢可能需要不同的緩存策略。一些解決方案包括:
- 智能緩存: 考慮使用能夠理解GraphQL查詢結構的緩存解決方案,如Apollo Server的緩存功能。
- 緩存指令: 使用GraphQL緩存指令來指定如何緩存特定的查詢或字段。
在傳統的RESTful API中,版本控制通常通過URL或請求頭來實現,例如
/v1/users
或/v2/users
。然而,在GraphQL中,由於GraphQL具備自描述性和靈活性,所以不需要使用硬性版本控制。但隨著業務需求的變化,可能會引入新的字段或類型,或者廢棄舊的字段。
根據GraphQL 最佳實踐,我們可以通過增量更新和逐漸廢棄舊版本(ex.@deprecated(reason: String)
),來實現版本控制和遷移
這種做法的好處是可以持續地演進GraphQL schema,而不會對現有的客戶端產生破壞性影響。通過只返回明確請求的數據,GraphQL允許我們通過添加新類型和新字段來增加新功能,而無需創建破壞性更改。
並且藉由提供無版本API,使我們在添加新功能時不需要發布新版本。來降低了維護多個API版本的複雜性,提高了API的可理解性和可維護性。
本文介紹了在設計和實施GraphQL架構時需要考慮的一些重要問題,包括N+1查詢問題、查詢的複雜度和深度限制、錯誤處理、授權和認證、緩存以及版本控制和遷移。