Filtering
It is possible to define filters for Django types, which will
be converted into .filter(...)
queries for the ORM:
import strawberry_djangofrom strawberry import auto
@strawberry_django.filter_type(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.
AND, OR, NOT, DISTINCT …
To every filter AND
, OR
, NOT
& DISTINCT
fields are added to allow more complex filtering
{ fruits( filters: { name: "kebab" OR: { name: "raspberry" } } ) { ... }}
List-based AND/OR/NOT Filters
The AND
, OR
, and NOT
operators can also be declared as lists, allowing for more complex combinations of conditions. This is particularly useful when you need to combine multiple conditions in a single operation.
@strawberry_django.filter_type(models.Vegetable, lookups=True)class VegetableFilter: id: auto name: auto AND: Optional[list[Self]] = strawberry.UNSET OR: Optional[list[Self]] = strawberry.UNSET NOT: Optional[list[Self]] = strawberry.UNSET
This enables queries like:
{ vegetables( filters: { AND: [{ name: { contains: "blue" } }, { name: { contains: "squash" } }] } ) { id }}
The list-based filtering system differs from the single object filter in a few ways:
- It allows combining multiple conditions in a single
AND
,OR
, orNOT
operation - The conditions in a list are evaluated together as a group
- When using
AND
, all conditions in the list must be satisfied - When using
OR
, any condition in the list can be satisfied - When using
NOT
, none of the conditions in the list should be satisfied
This is particularly useful for complex queries where you need to have multiple conditions against the same field.
Lookups
Lookups can be added to all fields with lookups=True
, which will
add more options to resolve each type. For example:
@strawberry_django.filter_type(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]
Filtering over relationships
@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}
Custom filter methods
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]
process_filters
As seen above
strawberry_django.process_filters
function is exposed and can be reused in custom methods. Above it’s used to resolve fields lookupsnull values
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 forNone
). This can be modified usingstrawberry_django.filter_field(filter_none=True)
This also means that built in
exact
&iExact
lookups cannot be used to filter forNone
andisNull
have to be used explicitly.value resolution
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 wellresolution 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}
Resolver arguments
-
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
Resolver return
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.
What about nulls?
By default null
values are ignored. This can be toggled as such @strawberry_django.filter_field(filter_none=True)
Overriding the default filter
method
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.
Adding filters to types
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.typeclass 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.
Adding filters directly into a field
Filters added into a field override the default filters of this type.
@strawberry.typeclass Query: fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter)
Generic Lookup reference
There is 7 already defined Generic Lookup strawberry.input
classes importable from strawberry_django
BaseFilterLookup
- contains
exact
,isNull
&inList
- used for
ID
&bool
fields
RangeLookup
- used for
range
orBETWEEN
filtering
ComparisonFilterLookup
- inherits
BaseFilterLookup
- additionaly contains
gt
,gte
,lt
,lte
, &range
- used for Numberical fields
FilterLookup
- inherits
BaseFilterLookup
- additionally contains
iExact
,contains
,iContains
,startsWith
,iStartsWith
,endsWith
,iEndsWith
,regex
&iRegex
- used for string based fields and as default
DateFilterLookup
- inherits
ComparisonFilterLookup
- additionally contains
year
,month
,day
,weekDay
,isoWeekDay
,week
,isoYear
&quarter
- used for date based fields
TimeFilterLookup
- inherits
ComparisonFilterLookup
- additionally contains
hour
,minute
,second
,date
&time
- used for time based fields
DatetimeFilterLookup
- inherits
DateFilterLookup
&TimeFilterLookup
- used for timedate based fields
Legacy filtering
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