在前面的說明主要著重在查詢(Read)的部分,API 的開發還會有新增(Ctreate)、修改(Update)、刪除(Delete),GraphQL 在這部分就是透過變更(Mutations)來達成。
GraphQL 使用 HTTP 協定進行資料傳輸,其中GET
方法主要支援查詢,POST
方法則支援查詢和變更。倘若情況允許,大多會使用 POST
方法傳輸,其主要原因在於 GET
方法的 URL 查詢參數存在長度限制。但在某些情況下,GET
方法也可能是更適合的選擇,例如當需要藉由瀏覽器對 GET 方法的快取回應資料以提升服務效能時。
下面我們先做一個簡單的變更(Mutation)的練習:
# app/services.py
# ... 省略
class UserService:
# ... 省略
+ def update_user(
+ self,
+ id: int,
+ email: str,
+ first_name: str,
+ last_name: str,
+ ) -> typing.Optional[types.User]:
+ user = self.user(id)
+ if user:
+ user.email = email
+ user.first_name = first_name
+ user.last_name = last_name
+ # Save to database
+ return user
+user_service = UserService()
# app/query.py
# ... 省略
-user_service = services.UserService()
# ... 省略
@strawberry.type
class Query:
- users: typing.List[types.User] = strawberry.field(resolver=user_service.all_users)
- user: typing.Optional[types.User] = strawberry.field(resolver=user_service.user)
+ users: typing.List[types.User] = strawberry.field(
+ resolver=services.user_service.all_users,
+ )
+ user: typing.Optional[types.User] = strawberry.field(
+ resolver=services.user_service.user,
+ )
# app/mutation.py
+import typing
+import strawberry
+from app import services, types
+@strawberry.type
+class Mutation:
+ update_user: typing.Optional[types.User] = strawberry.mutation(
+ resolver=services.user_service.update_user,
+ description="Update user",
+ )
# app/__init__.py
# ... 省略
-from app import query
+from app import query, mutation
__all__ = ["schema"]
schema = strawberry.Schema(
query=query.Query,
+ mutation=mutation.Mutation,
directives=[sensitive_text, replace],
)
mutation UserUpdateMutation(
$userId: Int!,
$userLastName: String!,
$userFirstName: String!,
$userEmail: String!,
) {
updateUser(
id: $userId,
lastName: $userLastName,
firstName: $userFirstName,
email: $userEmail,
) {
username
role
id
fullName
email
}
}
{
"userId": {替換成可用的 User ID},
"userFirstName": "User",
"userLastName": "Test",
"userEmail": "test@email.com"
}
一開始我們先在UserService
新增一個update_user
功能用來更新使用者資料,接著我們將原本宣告在app/query.py
的user_service
,改成宣告在app/services.py
,以利其他模組匯入使用,接著新增update_user
的變更功能,最後將Mutation
設定進Schema
,就是可以在 GraphiQL 的頁面上使用。
在之前說明型別系統的時候,有提到 Input Type 是用來包裝複雜的輸入資料用的物件型別,在上面的範例功能也可以看到引數ㄧ多就需要定義許多變數,所以大多數情況會定義一個引數叫input
的 Input Type 來當作 Mutation 的輸入,在 strawberry 有提供一個方便的功能自動將resolver
的引數轉成 Input Type,如下所示:
# app/mutation.py
# ... 省略
+from strawberry.field_extensions import InputMutationExtension
# ... 省略
@strawberry.type
class Mutation:
update_user: typing.Optional[types.User] = strawberry.mutation(
resolver=services.user_service.update_user,
description="Update user",
+ extensions=[InputMutationExtension()],
)
mutation UserUpdateMutation(
$input: UpdateUserInput!
) {
updateUser(input: $input) {
username
role
id
fullName
email
}
}
{
"input": {
"id": {替換成可用的 User ID},
"firstName": "User",
"lastName": "Test",
"email": "test@email.com"
}
}
strawberry 提供了欄位(變更)擴充(Extensions)介面,讓我們更有彈性地插入額外的功能,像是InputMutationExtension
就提供自動將resolver
的引數轉成 Input Type 的功能。
接下來我們試著建立有 Input Type 的變更功能,我們以新增一個新的一般使用者的功能為例:
# app/types.py
# ... 省略
+@strawberry.input
+class NormalUserInput:
+ username: str
+ email: str
+ first_name: str
+ last_name: str
+ password: str
+ phone: str
+ birthdate: datetime.date
+ address: typing.Optional[str] = None
# app/services.py
# ... 省略
class UserService:
# ... 省略
+ def new_normal_user(self, input: types.NormalUserInput) -> types.User:
+ detail = types.NormalUserDetail(
+ phone= input.phone,
+ birthdate= input.birthdate,
+ address= input.address,
+ )
+ user = types.User(
+ id=fake.random_int(min=1),
+ username=input.username,
+ email=input.email,
+ first_name=input.first_name,
+ last_name=input.last_name,
+ password=fake.lexify(text="?????????????????"), # Hashed password
+ role=types.UserRole.NORMAL,
+ detail=detail,
+ last_login=None
+ )
+ self.normal_users.append(user)
+ return user
# app/mutation.py
# ... 省略
@strawberry.type
class Mutation:
update_user: typing.Optional[types.User] = strawberry.mutation(
resolver=services.user_service.update_user,
description="Update user",
extensions=[InputMutationExtension()],
)
+ @strawberry.mutation(description="Create new normal user")
+ def new_normal_user(self, input: types.NormalUserInput) -> types.User:
+ return services.user_service.new_normal_user(input)
mutation NewUserMutation(
$input: NormalUserInput!,
) {
newNormalUser(input: $input) {
id
fullName
email
role
lastLogin
}
}
{
"input": {
"username": "newuser01",
"email": "newuser01@email.com",
"firstName": "New",
"lastName": "User01",
"password": "NewUser01Password",
"phone": "0987654321",
"birthdate": "2023-01-01",
"address": null
}
}
一開始我們先建立了一個NormalUserInput
的輸入型態,它包含建立一般使用者的資料必要欄位,其中address
欄位是非必填欄位,所以input
JSON 格式內此欄位可以不必出現,或是帶入null
(如上面 JSON 資料),接著我們在UserService
中新增new_normal_user
的功能,它所做的事,是先建立一般使用者的詳細資訊的物件,再建立一般使用者的物件,在密碼的欄位中我們使用faker
假裝密碼已經雜湊過了,最後將新的一般使用者加進一般使用者列表中,並回傳物件。
在這些變更(Mutation)範例中,我們可以觀察到異動資料的 API 回傳格式到某種程度上是由客戶端來定義。因此,在使用 GraphQL API 變更資料時,我們同時也能執行查詢。這個特點讓它相較於過去的 API 設計更具優勢,至少能省下一次呼叫查詢 API 的步驟。
上面的內容已經相當長了,所以完整程式碼請到 Github 上瀏覽: