iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0

https://ithelp.ithome.com.tw/upload/images/20231001/20161957I6zdsPpwNK.png

在上一篇的內容中可能會發現,透過 strawberry_django 自動轉換的 GraphQL 型態會缺少一些完整的型態定義,像是上圖的Post就缺少多對多關聯的欄位,而且關聯Userauthor欄位卻只有一個id欄位,下面就開始處理這些問題吧。

針對 strawberry_django 使用fieldsexclude會出現這種意料之外的轉換結果,並且當前套件的版本看起來不是穩定的版本,所以建議直接明確自行定義使用哪些欄位以及欄位的型態。

下面我們將現前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

https://ithelp.ithome.com.tw/upload/images/20231001/20161957JQBHS3oanj.png

上面將原本使用fieldsexclude的地方移除,改成我們自己一個一個自行定義,在Postauthor欄位,因為還沒有定義User型態,所以先使用author_idauthor_id這個欄位是 Django ORM 在ForeignKey欄位會自動產生的物件屬性 ,tagscategories是多對多的欄位,它們的值都會是回傳列表,所以型態要定義成該物件的 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)

https://ithelp.ithome.com.tw/upload/images/20231001/20161957235v5XPkw3.png

我們在Post上定義一個comments的欄位,它的 Resolver 就是他自己本身,Resolver 函式裡面self是 Django 模型本身,所以我們可以做進一步的 ORM 查詢操作,這邊看到使用comment_set來查詢留言,comment_set這個屬性名稱是因為我們沒有在Comment的 Django 模型上對應的 ForeignKey 上設定related_name參數,它預設會用模型名稱+_+set當作反向關聯的名稱。
後面的Categorypath欄位的 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

參考資料


上一篇
Day 15:Strawberry Django 定義型態與查詢
下一篇
Day 17:Strawberry Django 排序與分頁
系列文
Django 與 Strawberry GraphQL:探索現代 API 開發之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言