在開始這篇的練習之前,可以先在 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,
)
# ... 省略
修改完後,我們先看一下使用篩選前的查詢結果:
接著使用資料篩選:
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
裡面除了我們定義的欄位以外,還會有AND
、OR
等操作可以,讓可以做更加複雜的查詢。
接下來如果想要透過標籤篩選文章,就是透過設定關聯的篩選來達成,下面修改文章的篩選類別:
# 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"]
# ... 省略
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
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]
# ... 省略
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
# ... 省略
上面加了一個簡易的搜尋功能。
這次修改內容可以參考 Git commit:https://github.com/JiaWeiXie/django-graphql-tutorial/commit/0391081075600ac3a18362088a815772f1e6fa0a