title |
---|
Filtering |
It is possible to define filters for Django types, which will
be converted into .filter(...)
queries for the ORM:
import strawberry_django
from strawberry import auto
@strawberry_django.filter(models.Fruit)
class FruitFilter:
id: auto
name: auto
@strawberry_django.type(models.Fruit, filters=FruitFilter)
class Fruit:
...
Tip
In most cases filter fields should have Optional
annotations and default value strawberry.UNSET
like so:
foo: Optional[SomeType] = strawberry.UNSET
Above auto
annotation is wrapped in Optional
automatically.
UNSET
is automatically used for fields without field
or with strawberry_django.filter_field
.
The code above would generate following schema:
input FruitFilter {
id: ID
name: String
AND: FruitFilter
OR: FruitFilter
NOT: FruitFilter
DISTINCT: Boolean
}
Tip
If you are using the relay integration and working with types inheriting
from relay.Node
and GlobalID
for identifying objects, you might want to set
MAP_AUTO_ID_AS_GLOBAL_ID=True
in your strawberry django settings
to make sure auto
fields gets mapped to GlobalID
on types and filters.
To every filter AND
, OR
, NOT
& DISTINCT
fields are added to allow more complex filtering
{
fruits(
filters: {
name: "kebab"
OR: {
name: "raspberry"
}
}
) { ... }
}
Lookups can be added to all fields with lookups=True
, which will
add more options to resolve each type. For example:
@strawberry_django.filter(models.Fruit, lookups=True)
class FruitFilter:
id: auto
name: auto
The code above would generate the following schema:
input IDBaseFilterLookup {
exact: ID
isNull: Boolean
inList: [String!]
}
input StrFilterLookup {
exact: ID
isNull: Boolean
inList: [String!]
iExact: String
contains: String
iContains: String
startsWith: String
iStartsWith: String
endsWith: String
iEndsWith: String
regex: String
iRegex: String
}
input FruitFilter {
id: IDFilterLookup
name: StrFilterLookup
AND: FruitFilter
OR: FruitFilter
NOT: FruitFilter
DISTINCT: Boolean
}
Single-field lookup can be annotated with the FilterLookup
generic type.
from strawberry_django import FilterLookup
@strawberry_django.filter(models.Fruit)
class FruitFilter:
name: FilterLookup[str]
@strawberry_django.filter(models.Color)
class ColorFilter:
id: auto
name: auto
@strawberry_django.filter(models.Fruit)
class FruitFilter:
id: auto
name: auto
color: ColorFilter | None
The code above would generate following schema:
input ColorFilter {
id: ID
name: String
AND: ColorFilter
OR: ColorFilter
NOT: ColorFilter
}
input FruitFilter {
id: ID
name: String
color: ColorFilter
AND: FruitFilter
OR: FruitFilter
NOT: FruitFilter
}
You can define custom filter method by defining your own resolver.
@strawberry_django.filter(models.Fruit)
class FruitFilter:
name: auto
last_name: auto
@strawberry_django.filter_field
def simple(self, value: str, prefix) -> Q:
return Q(**{f"{prefix}name": value})
@strawberry_django.filter_field
def full_name(
self,
queryset: QuerySet,
value: str,
prefix: str
) -> tuple[QuerySet, Q]:
queryset = queryset.alias(
_fullname=Concat(
f"{prefix}name", Value(" "), f"{prefix}last_name"
)
)
return queryset, Q(**{"_fullname": value})
@strawberry_django.filter_field
def full_name_lookups(
self,
info: Info,
queryset: QuerySet,
value: strawberry_django.FilterLookup[str],
prefix: str
) -> tuple[QuerySet, Q]:
queryset = queryset.alias(
_fullname=Concat(
f"{prefix}name", Value(" "), f"{prefix}last_name"
)
)
return strawberry_django.process_filters(
filters=value,
queryset=queryset,
info=info,
prefix=f"{prefix}_fullname"
)
Warning
It is discouraged to use queryset.filter()
directly. When using more
complex filtering via NOT
, OR
& AND
this might lead to undesired behaviour.
Tip
As seen above strawberry_django.process_filters
function is exposed and can be
reused in custom methods. Above it's used to resolve fields lookups
By default null
value is ignored for all filters & lookups. This applies to custom
filter methods as well. Those won't even be called (you don't have to check for None
).
This can be modified using
strawberry_django.filter_field(filter_none=True)
This also means that built in exact
& iExact
lookups cannot be used to filter for None
and isNull
have to be used explicitly.
value
parameter of typerelay.GlobalID
is resolved to itsnode_id
attributevalue
parameter of typeEnum
is resolved to is's value- above types are converted in
lists
as well
resolution can modified via strawberry_django.filter_field(resolve_value=...)
- True - always resolve
- False - never resolve
- UNSET (default) - resolves for filters without custom method only
The code above generates the following schema:
input FruitFilter {
name: String
lastName: String
simple: str
fullName: str
fullNameLookups: StrFilterLookup
}
prefix
- represents the current path or position- Required
- Important for nested filtering
- In code bellow custom filter
name
ends up filteringFruit
instead ofColor
without applyingprefix
@strawberry_django.filter(models.Fruit)
class FruitFilter:
name: auto
color: ColorFilter | None
@strawberry_django.filter(models.Color)
class ColorFilter:
@strawberry_django.filter_field
def name(self, value: str, prefix: str):
# prefix is "fruit_set__" if unused root object is filtered instead
if value:
return Q(name=value)
return Q()
{
fruits( filters: {color: name: "blue"} ) { ... }
}
value
- represents graphql field type- Required, but forbidden for default
filter
method - must be annotated
- used instead of field's return type
- Required, but forbidden for default
queryset
- can be used for more complex filtering- Optional, but Required for default
filter
method - usually used to
annotate
QuerySet
- Optional, but Required for default
For custom field methods two return values are supported
- django's
Q
object - tuple with
QuerySet
and django'sQ
object ->tuple[QuerySet, Q]
For default filter
method only second variant is supported.
By default null
values are ignored. This can be toggled as such @strawberry_django.filter_field(filter_none=True)
Works similar to field filter method, but:
- is responsible for resolution of filtering for entire object
- must be named
filter
- argument
queryset
is Required - argument
value
is Forbidden
@strawberry_django.filter(models.Fruit)
class FruitFilter:
def ordered(
self,
value: int,
prefix: str,
queryset: QuerySet,
):
queryset = queryset.alias(
_ordered_num=Count(f"{prefix}orders__id")
)
return queryset, Q(**{f"{prefix}_ordered_num": value})
@strawberry_django.order_field
def filter(
self,
info: Info,
queryset: QuerySet,
prefix: str,
) -> tuple[QuerySet, list[Q]]:
queryset = queryset.filter(
... # Do some query modification
)
return strawberry_django.process_filters(
self,
info=info,
queryset=queryset,
prefix=prefix,
skip_object_order_method=True
)
Tip
As seen above strawberry_django.process_filters
function is exposed and can be
reused in custom methods.
For filter method filter
skip_object_order_method
was used to avoid endless recursion.
All fields and CUD mutations inherit filters from the underlying type by default. So, if you have a field like this:
@strawberry_django.type(models.Fruit, filters=FruitFilter)
class Fruit:
...
@strawberry.type
class Query:
fruits: list[Fruit] = strawberry_django.field()
The fruits
field will inherit the filters
of the type in the same way as
if it was passed to the field.
Filters added into a field override the default filters of this type.
@strawberry.type
class Query:
fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter)
There is 7 already defined Generic Lookup strawberry.input
classes importable from strawberry_django
- contains
exact
,isNull
&inList
- used for
ID
&bool
fields
- used for
range
orBETWEEN
filtering
- inherits
BaseFilterLookup
- additionaly contains
gt
,gte
,lt
,lte
, &range
- used for Numberical fields
- inherits
BaseFilterLookup
- additionally contains
iExact
,contains
,iContains
,startsWith
,iStartsWith
,endsWith
,iEndsWith
,regex
&iRegex
- used for string based fields and as default
- inherits
ComparisonFilterLookup
- additionally contains
year
,month
,day
,weekDay
,isoWeekDay
,week
,isoYear
&quarter
- used for date based fields
- inherits
ComparisonFilterLookup
- additionally contains
hour
,minute
,second
,date
&time
- used for time based fields
- inherits
DateFilterLookup
&TimeFilterLookup
- used for timedate based fields
The previous version of filters can be enabled via USE_DEPRECATED_FILTERS
Warning
If USE_DEPRECATED_FILTERS is not set to True
legacy custom filtering
methods will be not be called.
When using legacy filters it is important to use legacy
strawberry_django.filters.FilterLookup
lookups as well.
The correct version is applied for auto
annotated filter field (given lookups=True
being set). Mixing old and new lookups
might lead to error DuplicatedTypeName: Type StrFilterLookup is defined multiple times in the schema
.
While legacy filtering is enabled new filtering custom methods are
fully functional including default filter
method.
Migration process could be composed of these steps:
- enable USE_DEPRECATED_FILTERS
- gradually transform custom filter field methods to new version (do not forget to use old FilterLookup if applicable)
- gradually transform default
filter
methods - disable USE_DEPRECATED_FILTERS - This is breaking change