iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

由前向後,從前端邁向全端系列 第 29

29.【從前端到全端,Nextjs+Nestjs】改進和解決GraphQL問題

  • 分享至 

  • xImage
  •  

文章重點

  • GraphQL常見問題

本文

在設計GraphQL架構時,有一些常見的問題需要注意,以確保系統的效能和安全性。以下列出了一些須留意的GraphQL問題:

1.N+1 查詢問題:

在GraphQL中,N+1查詢問題是一個常見的效能問題,它發生在server為每個項目的列表中的每個項目執行單獨的查詢時。解決這個問題的一種方法是使用dataloader來批量處理查詢,減少查詢次數。

Prisma有一篇關於介紹解決n+1問題的文章。他提供的建議是:

  • 使用includeselect降低查詢複雜度。如果model有與其他模型的關聯,我們能考慮使用includeselect來優化查詢
  • 需要處理大量數據時,使用Prisma的createMany, deleteMany, updateMany, 或 findMany方法可以提高效能。並且可以在單個操作中處理多條記錄,而不是單獨處理每條記錄

2. 複雜度和深度限制:

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

3. 錯誤處理:

在GraphQL中,一個重要的特點是即使在發生錯誤時,它也會返回一個200 OK HTTP狀態碼,與REST不同,REST會返回如400或500等特定的錯誤狀態碼。此外,GraphQL允許在單個請求中執行多個操作,因此可能會有多個錯誤。為了有效處理這些錯誤,應該:

  • 錯誤分類: 明確區分客戶端錯誤(例如,查詢格式錯誤)和服務器錯誤(例如,資料庫連接失敗),並通過不同的錯誤碼或消息來表示它們。
  • 詳細的錯誤信息: 提供足夠的錯誤信息,以便開發人員和系統管理員可以快速識別和解決問題。

4. 授權和認證:

在GraphQL中,授權和認證的實施可能會比RESTful API更為複雜,因為GraphQL允許客戶端指定它們想要什麼資料,而RESTful API通常有固定的端點和返回結構。解決方案包括:

  • 字段級別的授權: 考慮在字段級別實施授權,以確保只有具有適當權限的用戶可以訪問敏感資料。
  • 使用身份驗證中間件: 確保在解析器執行之前驗證用戶的身份。

5. 緩存問題:

由於GraphQL查詢的靈活性,它的緩存實施可能會比RESTful API更為複雜。例如,同一個資源可以通過多個不同的查詢獲取,每個查詢可能需要不同的緩存策略。一些解決方案包括:

  • 智能緩存: 考慮使用能夠理解GraphQL查詢結構的緩存解決方案,如Apollo Server的緩存功能。
  • 緩存指令: 使用GraphQL緩存指令來指定如何緩存特定的查詢或字段。

6. 版本控制和遷移:

在傳統的RESTful API中,版本控制通常通過URL或請求頭來實現,例如/v1/users/v2/users。然而,在GraphQL中,由於GraphQL具備自描述性和靈活性,所以不需要使用硬性版本控制。但隨著業務需求的變化,可能會引入新的字段或類型,或者廢棄舊的字段。

根據GraphQL 最佳實踐,我們可以通過增量更新逐漸廢棄舊版本(ex.@deprecated(reason: String)),來實現版本控制和遷移

這種做法的好處是可以持續地演進GraphQL schema,而不會對現有的客戶端產生破壞性影響。通過只返回明確請求的數據,GraphQL允許我們通過添加新類型和新字段來增加新功能,而無需創建破壞性更改。

並且藉由提供無版本API,使我們在添加新功能時不需要發布新版本。來降低了維護多個API版本的複雜性,提高了API的可理解性和可維護性。


總結

本文介紹了在設計和實施GraphQL架構時需要考慮的一些重要問題,包括N+1查詢問題、查詢的複雜度和深度限制、錯誤處理、授權和認證、緩存以及版本控制和遷移。


上一篇
28.【從前端到全端,Nextjs+Nestjs】將Nextjs的GraphQL資料改成使用後端資料
下一篇
30.【從前端到全端,Nextjs+Nestjs】完結感言與未來規劃
系列文
由前向後,從前端邁向全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言