在上一篇的內容中可能會發現,透過 strawberry_django 自動轉換的 GraphQL 型態會缺少一些完整的型態定義,像是上圖的Post
就缺少多對多關聯的欄位,而且關聯User
的author
欄位卻只有一個id欄位,下面就開始處理這些問題吧。
針對 strawberry_django 使用fields
或exclude
會出現這種意料之外的轉換結果,並且當前套件的版本看起來不是穩定的版本,所以建議直接明確自行定義使用哪些欄位以及欄位的型態。
下面我們將現前server/app/blog/graph/types.py
的內容進行修改:
+import datetime
+import typing
+import uuid
+import strawberry
import strawberry_django
from server.app.blog import models as blog_models
__all__ = (
"Post",
"Comment",
"Tag",
"Category",
)
-@strawberry_django.type(blog_models.Post, fields="__all__")
+@strawberry_django.type(blog_models.Post)
class Post:
+ id: uuid.UUID # noqa: A003
+ slug: str
+ author_id: strawberry.ID
+ title: str
+ content: str
+ published_at: datetime.datetime | None
+ published: bool | None
+ tags: list["Tag"]
+ categories: list["Category"]
-@strawberry_django.type(blog_models.Comment, fields="__all__")
+@strawberry_django.type(blog_models.Comment)
class Comment:
+ id: uuid.UUID # noqa: A003
+ post: Post
+ parent: typing.Optional["Comment"]
+ author_id: strawberry.ID | None
+ content: str
-@strawberry_django.type(blog_models.Tag, fields=["name"])
+@strawberry_django.type(blog_models.Tag)
class Tag:
+ name: str
-@strawberry_django.type(
- blog_models.Category,
- exclude=["created_at", "motified_at"],
-)
+@strawberry_django.type(blog_models.Category)
class Category:
+ id: uuid.UUID # noqa: A003
+ slug: str
+ parent: typing.Optional["Category"]
+ name: str
上面將原本使用fields
或exclude
的地方移除,改成我們自己一個一個自行定義,在Post
的author
欄位,因為還沒有定義User
型態,所以先使用author_id
,author_id
這個欄位是 Django ORM 在ForeignKey
欄位會自動產生的物件屬性 ,tags
與categories
是多對多的欄位,它們的值都會是回傳列表,所以型態要定義成該物件的 GraphQL 型態的列表。
接下來想要在文章下面可以直接查詢文章的留言,以及分類的路徑名稱,所以我們繼續在server/app/blog/graph/types.py
增加功能:
# ... 省略
@strawberry_django.type(blog_models.Post)
class Post:
# ... 省略
+ @strawberry_django.field
+ def comments(self) -> list["Comment"]:
+ return self.comment_set.all() # type: ignore
# ... 省略
@strawberry_django.type(blog_models.Category)
class Category:
# ... 省略
+ @strawberry_django.field
+ def path(self) -> str:
+ return str(self)
我們在Post
上定義一個comments
的欄位,它的 Resolver 就是他自己本身,Resolver 函式裡面self
是 Django 模型本身,所以我們可以做進一步的 ORM 查詢操作,這邊看到使用comment_set
來查詢留言,comment_set
這個屬性名稱是因為我們沒有在Comment
的 Django 模型上對應的 ForeignKey 上設定related_name
參數,它預設會用模型名稱
+_
+set
當作反向關聯的名稱。
後面的Category
的path
欄位的 Resolver 函式裡面,因為我們在Category
的 Django 模型上已經定義好__str__
的顯示文字格式,所以我們用str
去轉換self
就會呼叫到__str__
。
我們會發現Post
上面有許多關聯的物件,如果我們需要預先把那些關聯的欄位用 SQL Join 起來該怎麼做呢?
為了查詢上的差異,我們先安裝 Django 額外的開發輔助套件,來幫助我們方便看 SQL 查詢:
$ poetry add django-extensions
$ poetry add Werkzeug --group dev
接著設定server/settings.py
:
# ... 省略
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ "django_extensions",
"server.app.blog",
]
然後我們runserver
的指令改成:
$ python manage.py runserver_plus --print-sql
我先執行一次 GraphQL 查詢:
query MyQuery {
posts {
id
title
slug
content
authorId
published
publishedAt
tags {
name
}
categories {
name
slug
path
}
comments {
content
authorId
}
}
}
在runserver
的終端機會輸出查詢 SQL 語法:
SELECT "blog_post"."id",
"blog_post"."created_at",
"blog_post"."motified_at",
"blog_post"."slug",
"blog_post"."author_id",
"blog_post"."title",
"blog_post"."content",
"blog_post"."published_at",
"blog_post"."published"
FROM "blog_post"
ORDER BY "blog_post"."created_at" DESC
Execution time: 0.000592s [Database: default]
SELECT "auth_user"."id",
"auth_user"."password",
"auth_user"."last_login",
"auth_user"."is_superuser",
"auth_user"."username",
"auth_user"."first_name",
"auth_user"."last_name",
"auth_user"."email",
"auth_user"."is_staff",
"auth_user"."is_active",
"auth_user"."date_joined"
FROM "auth_user"
WHERE "auth_user"."id" = 1
LIMIT 21
Execution time: 0.000333s [Database: default]
SELECT "blog_tag"."id",
"blog_tag"."created_at",
"blog_tag"."motified_at",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_post_tags"
ON ("blog_tag"."id" = "blog_post_tags"."tag_id")
WHERE "blog_post_tags"."post_id" = '6119c2f3cd7648128de771a13788140a'
ORDER BY "blog_tag"."created_at" DESC
Execution time: 0.000397s [Database: default]
SELECT "blog_category"."id",
"blog_category"."created_at",
"blog_category"."motified_at",
"blog_category"."slug",
"blog_category"."parent_id",
"blog_category"."name"
FROM "blog_category"
INNER JOIN "blog_post_categories"
ON ("blog_category"."id" = "blog_post_categories"."category_id")
WHERE "blog_post_categories"."post_id" = '6119c2f3cd7648128de771a13788140a'
ORDER BY "blog_category"."created_at" DESC
Execution time: 0.001197s [Database: default]
SELECT "blog_category"."id",
"blog_category"."created_at",
"blog_category"."motified_at",
"blog_category"."slug",
"blog_category"."parent_id",
"blog_category"."name"
FROM "blog_category"
WHERE "blog_category"."id" = 'b08e0ee60e924520aa9a90553333e657'
LIMIT 21
Execution time: 0.000141s [Database: default]
SELECT "blog_comment"."id",
"blog_comment"."created_at",
"blog_comment"."motified_at",
"blog_comment"."post_id",
"blog_comment"."parent_id",
"blog_comment"."author_id",
"blog_comment"."content"
FROM "blog_comment"
WHERE "blog_comment"."post_id" = '6119c2f3cd7648128de771a13788140a'
ORDER BY "blog_comment"."created_at" DESC
Execution time: 0.000459s [Database: default]
接著只修改server/app/blog/graph/types.py
裡面的Post
,其他的就先照舊:
+from django.db.models import QuerySet
+from strawberry.types import Info
# ... 省略
@strawberry_django.type(blog_models.Post)
class Post:
# ... 省略
+ @classmethod
+ def get_queryset(
+ cls,
+ queryset: QuerySet[blog_models.Post],
+ info: Info,
+ **kwargs: typing.Any,
+ ) -> QuerySet[blog_models.Post]:
+ return queryset.select_related("author").prefetch_related(
+ "tags", "categories", "comment_set"
+ )
修改完成後,我們就可以重啟runserver
,再做一次一樣的 GraphQL 查詢,我再一次輸出的 SQL 查詢語法:
SELECT "blog_post"."id",
"blog_post"."created_at",
"blog_post"."motified_at",
"blog_post"."slug",
"blog_post"."author_id",
"blog_post"."title",
"blog_post"."content",
"blog_post"."published_at",
"blog_post"."published",
"auth_user"."id",
"auth_user"."password",
"auth_user"."last_login",
"auth_user"."is_superuser",
"auth_user"."username",
"auth_user"."first_name",
"auth_user"."last_name",
"auth_user"."email",
"auth_user"."is_staff",
"auth_user"."is_active",
"auth_user"."date_joined"
FROM "blog_post"
INNER JOIN "auth_user"
ON ("blog_post"."author_id" = "auth_user"."id")
ORDER BY "blog_post"."created_at" DESC
Execution time: 0.001281s [Database: default]
SELECT ("blog_post_tags"."post_id") AS "_prefetch_related_val_post_id",
"blog_tag"."id",
"blog_tag"."created_at",
"blog_tag"."motified_at",
"blog_tag"."name"
FROM "blog_tag"
INNER JOIN "blog_post_tags"
ON ("blog_tag"."id" = "blog_post_tags"."tag_id")
WHERE "blog_post_tags"."post_id" IN ('6119c2f3cd7648128de771a13788140a')
ORDER BY "blog_tag"."created_at" DESC
Execution time: 0.000124s [Database: default]
SELECT ("blog_post_categories"."post_id") AS "_prefetch_related_val_post_id",
"blog_category"."id",
"blog_category"."created_at",
"blog_category"."motified_at",
"blog_category"."slug",
"blog_category"."parent_id",
"blog_category"."name"
FROM "blog_category"
INNER JOIN "blog_post_categories"
ON ("blog_category"."id" = "blog_post_categories"."category_id")
WHERE "blog_post_categories"."post_id" IN ('6119c2f3cd7648128de771a13788140a')
ORDER BY "blog_category"."created_at" DESC
Execution time: 0.000562s [Database: default]
SELECT "blog_comment"."id",
"blog_comment"."created_at",
"blog_comment"."motified_at",
"blog_comment"."post_id",
"blog_comment"."parent_id",
"blog_comment"."author_id",
"blog_comment"."content"
FROM "blog_comment"
WHERE "blog_comment"."post_id" IN ('6119c2f3cd7648128de771a13788140a')
ORDER BY "blog_comment"."created_at" DESC
Execution time: 0.000360s [Database: default]
SELECT "blog_category"."id",
"blog_category"."created_at",
"blog_category"."motified_at",
"blog_category"."slug",
"blog_category"."parent_id",
"blog_category"."name"
FROM "blog_category"
WHERE "blog_category"."id" = 'b08e0ee60e924520aa9a90553333e657'
LIMIT 21
Execution time: 0.000124s [Database: default]
可以看到它有執行我們指定 ORM 查詢操作,有做相關的 SQL Join。
再之後的內容會在更進一步說明查詢優化的部分,這篇主要在說明 strawberry_django 跟 GraphQL 查詢有關的功能。
這次修改內容可以參考 Git commit https://github.com/JiaWeiXie/django-graphql-tutorial/commit/c83ba05abcd320917d7de53029e98eb69e019d81