iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0

在開始這篇的練習之前,可以先在 Django admin 的頁面上新增一些使用者、文章、留言、分類以及標籤。

以往想到 Django 篩選資料的套件,大部分第一個直覺就是使用 django-filter [1],它提供類似定義 ModelForm 的方式,來定義有哪些欄位可以被篩選,甚至自定義處理資料篩選的方法,不過很可惜的是 strawberry_django 是沒有跟 django-filter 整合的,但是還好它內建有提供類似 django-filter 的篩選功能,並且更符合 GraphQL 的風格。

一開始我先建立一個新的 Python 檔,來放跟資料篩選相關的程式:

$ touch server/app/blog/graph/filters.py

接著編輯filters.py

import strawberry
import strawberry_django

from server.app.blog import models as blog_models

__all__ = (
    "PostFilter",
    "TagFilter",
)

@strawberry_django.filter(blog_models.Post)
class PostFilter:
    id: strawberry.auto  # noqa: A003
    slug: strawberry.auto

@strawberry_django.filter(blog_models.Tag)
class TagFilter:
    name: strawberry.auto

可以看到定義資料篩選的欄位方式跟前面的定義排序是類似的,而它使用的方式也是跟排序類似,一樣可以在 strawberry_django 欄位或是型態上面設定,這邊就將文章的篩選設定在文章的查詢上面,標籤的篩選設定在標籤的型態上面:

# server/app/blog/graph/types.py
# ... 省略
from server.app.authentication.graph import types as auth_types
from server.app.blog import models as blog_models
+from server.app.blog.graph import filters as blog_filters
from server.app.blog.graph import orders as blog_orders
# ... 省略

@strawberry_django.type(
    blog_models.Tag,
    order=blog_orders.TagOrder,
+   filters=blog_filters.TagFilter,
)
class Tag:
    name: str
# server/app/blog/graph/queries.py
# ... 省略
+from server.app.blog.graph import filters as blog_filters
from server.app.blog.graph import orders as blog_orders
from server.app.blog.graph import types as blog_types
# ... 省略
@strawberry.type
class Query:
    posts: list[blog_types.Post] = strawberry_django.field(
        order=blog_orders.PostOrder,
        pagination=True,
+       filters=blog_filters.PostFilter,
    )
		# ... 省略

修改完後,我們先看一下使用篩選前的查詢結果:

https://ithelp.ithome.com.tw/upload/images/20231003/2016195772p1zajOSf.png

接著使用資料篩選:

https://ithelp.ithome.com.tw/upload/images/20231003/20161957gRAqzyxEii.png

query MyQuery(
  $slug1: String,
  $slug2: String,
  $tag: String
) {
  posts(
    order: {publishedAt: DESC}
    pagination: {limit: 10, offset: 0}
    filters: {
      OR: {slug: $slug2},
      slug: $slug1
    }
  ) {
    id
    slug
    title
    tags(filters: {name: $tag}) {
      name
    }
  }
}

可以看到查詢引數會多出filters這個參數,filters裡面除了我們定義的欄位以外,還會有ANDOR等操作可以,讓可以做更加複雜的查詢。

接下來如果想要透過標籤篩選文章,就是透過設定關聯的篩選來達成,下面修改文章的篩選類別:

# server/app/blog/graph/filters.py
# ... 省略
@strawberry_django.filter(blog_models.Post)
class PostFilter:
    id: strawberry.auto  # noqa: A003
    slug: strawberry.auto
+   tags: typing.Optional["TagFilter"]
# ... 省略

https://ithelp.ithome.com.tw/upload/images/20231003/201619577Pub8Pl08Q.png

query MyQuery($tag: String) {
  posts(
    order: {publishedAt: DESC}
    pagination: {limit: 10, offset: 0}
    filters: {tags: {name: $tag}}
  ) {
    id
    slug
    title
    tags {
      name
    }
  }
}

上面已經增加使用標籤名稱篩選文章的功能,那接下來有個新的問題,如果想要使用標籤名稱尋找匹配特定符合的名稱的方式來篩選,即使用 Django QuerySet 相關尋找的方法 [2],

那麼我們應該怎麼做?

strawberry_django 中最簡單的方式是在篩選上設定lookups,下面就試著幫標籤篩選類別加上這個功能:

# server/app/blog/graph/filters.py
# ... 省略
-@strawberry_django.filter(blog_models.Tag)
+@strawberry_django.filter(blog_models.Tag, lookups=True)
class TagFilter:
    name: strawberry.auto

https://ithelp.ithome.com.tw/upload/images/20231003/20161957Alh5iSPcH1.png

query MyQuery($tag: String) {
  posts(order: {publishedAt: DESC}, pagination: {limit: 10, offset: 0}) {
    id
    slug
    title
    tags(
      filters: {name: {endsWith: $tag}}
    ) {
      name
    }
  }
}

設定lookups=True後,篩選類別裡面定義的欄位都會轉換成FilterLookup的型態。

那如果只想要某個欄位支援FilterLookup型態,那可以只定義在特定欄位設定FilterLookup

# server/app/blog/graph/filters.py
# ... 省略
@strawberry_django.filter(blog_models.Post)
class PostFilter:
    id: strawberry.auto  # noqa: A003
    slug: strawberry.auto
    tags: typing.Optional["TagFilter"]
+   title: FilterLookup[str]
# ... 省略

https://ithelp.ithome.com.tw/upload/images/20231003/20161957gOFKDdUYgJ.png

query MyQuery($title: String) {
  posts(filters: {
    title: {contains: $title}}
  ) {
    id
    slug
    title
  }
}

上面我們只幫標題定義成FilterLookup型態,所以就只有標題有許多尋找資料的操作方法。

那我們不想要出現這麼多尋找資料的操作方法,可以用以下方式:

# server/app/blog/graph/filters.py
# ... 省略
+@strawberry.input
+class TitleFilterLookup:
+   contains: str | None = strawberry.UNSET
+   in_list: list[str] | None = strawberry.UNSET

@strawberry_django.filter(blog_models.Post)
class PostFilter:
    id: strawberry.auto  # noqa: A003
    slug: strawberry.auto
    tags: typing.Optional["TagFilter"]
-   title: FilterLookup[str]
+   title: TitleFilterLookup
# ... 省略

我們照著FilterLookup型態的類別,自己重新定義一個FilterLookup型態,如上面的TitleFilterLookup型態,就可以只出現我們需要尋找資料的操作。

最後是自定義篩選欄位,並實作自定義篩選功能:

# server/app/blog/graph/filters.py
# ... 省略
import strawberry
import strawberry_django
+from django.db.models import Q, QuerySet

from server.app.blog import models as blog_models
# ... 省略
@strawberry_django.filter(blog_models.Post)
class PostFilter:
    id: strawberry.auto  # noqa: A003
    slug: strawberry.auto
    tags: typing.Optional["TagFilter"]
-   title: TitleFilterLookup
+		title: TitleFilterLookup | None
+   search: str | None

+   def filter_search(
+       self,
+       queryset: QuerySet[blog_models.Post],
+   ) -> QuerySet[blog_models.Post]:
+       if self.search:
+           queryset = queryset.filter(
+               Q(title__icontains=self.search)
+               | Q(content__icontains=self.search)
+               | Q(tags__name__icontains=self.search)
+               | Q(categories__name__icontains=self.search),
+           )
+
+       return queryset
# ... 省略

https://ithelp.ithome.com.tw/upload/images/20231003/20161957KivXq6l3LG.png

上面加了一個簡易的搜尋功能。

這次修改內容可以參考 Git commit:https://github.com/JiaWeiXie/django-graphql-tutorial/commit/0391081075600ac3a18362088a815772f1e6fa0a

參考資料


上一篇
Day 17:Strawberry Django 排序與分頁
下一篇
Day 19:Strawberry Django 新增、修改、刪除的變更
系列文
Django 與 Strawberry GraphQL:探索現代 API 開發之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言