首页 > 解决方案 > 使用graphene-django时如何将Relay的分页功能与django-filter中的filterset_class一起使用

问题描述

我有一个 Django 项目,我在其中使用 django-graphene 创建 GraphQL API。

尝试与(这是分页功能的核心DjangoFilterConnectionField一起使用时出现问题relay.Connection

我的模型太大并且有很多关系,但让我们保持简单......

class Pattern(models.Model):
    code = models.CharField(
        max_length=15
    )
    name = models.CharField(
        max_length=50
    )
    slug = AutoSlugField(
        populate_from='name',
        max_length=150
    )
    ...

我的节点看起来像:

class PatternNode(DjangoObjectType):
    # Many fields here...
    ...

    class Meta:
        model = Pattern
        interfaces = (relay.Node,)
        filterset_class = PatternFilterSet

如您所见,我已经filterset_class在我的 Node.js 中设置了属性Meta

所以,这是过滤器集:

class PatternFilterSet(FilterSet):
    order_by = OrderingFilter(
        fields=(
            ('date', 'date'),
            ('name', 'name'),
        )
    )
    productcategorization__design__contains = CharFilter(method="product_categorization_design_filter")
    products__predominant_colors__contains = CharFilter(method="products_predominant_colors_filter")

    class Meta:
        model = Pattern
        fields = {
            'name': ['exact', 'icontains', 'istartswith'],
            'alt_name': ['exact', 'icontains', 'istartswith'],
            'slug': ['exact'],
            'pattern_class': ['exact'],
            'sectors': ['exact', 'in'],
            'products__instances': ['exact'],
            'productcategorization__business': ['exact'],
            'productcategorization__market_segment': ['exact', 'in'],
        }

    @staticmethod
    def product_categorization_design_filter(queryset, name, value):
        """
        Does a productcategorization__design__contains filter "manually" because adding it in the Meta.fields does not
        work for ArrayField.

        Args:
             queryset (patterns.managers.PatternQuerySet)
             name (str)
             value (Array) comma delimited list of designs

        Returns:
            filtered_queryset (QuerySet)
        """
        return queryset.filter(productcategorization__design__contains=value.split(","))

    @staticmethod
    def products_predominant_colors_filter(queryset, name, value):
        """
        Does a products__predominant_colors__contains filter "manually" because adding it in the Meta.fields does not
        work for ArrayField.

        Args:
             queryset (patterns.managers.PatternQuerySet)
             name (str)
             value (Array) comma delimited list of designs

        Returns:
            filtered_queryset (QuerySet)
        """
        return queryset.filter(products__predominant_colors__contains=value.split(",")).distinct()

如您所见,我的 API 中需要针对该特定模型的许多特殊过滤选项。

在我的架构中,我有以下内容:

class PatternConnection(relay.Connection):
    class Meta:
        node = PatternNode


class Query(graphene.ObjectType):
    pattern = relay.Node.Field(
        PatternNode,
        id=ID(),
        slug=String()
    )
    patterns = relay.ConnectionField(PatternConnection)

此时一切正常,但过滤器不起作用。

我正在执行以下查询:

query Patterns {
    patterns(first: 2) {
        pageInfo {
            startCursor
            endCursor
            hasNextPage
        }
        edges {
            cursor
            node {
                id
                name
            }
        }
    }
}

并收到以下回复:

{
    "data": {
        "patterns": {
            "pageInfo": {
                "startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
                "endCursor": "YXJyYXljb25uZWN0aW9uOjE=",
                "hasNextPage": true
            },
            "edges": [
                {
                    "cursor": "YXJyYXljb25uZWN0aW9uOjA=",
                    "node": {
                        "id": "UGF0dGVybk5vZGU6Mjcw",
                        "name": "42 Oz - Jk"
                    }
                },
                {
                    "cursor": "YXJyYXljb25uZWN0aW9uOjE=",
                    "node": {
                        "id": "UGF0dGVybk5vZGU6Mjcx",
                        "name": "42 Oz - Pebble Top - Jk"
                    }
                }
            ]
        }
    }
}

所以分页运行良好!

现在,当我使用我的一个过滤器尝试它时,如下所示:

query Patterns ($predominantColors: String) {
    patterns(first: 2, products_PredominantColors_Contains: $predominantColors) {
        pageInfo {
            startCursor
            endCursor
            hasNextPage
        }
        edges {
            cursor
            node {
                id
                name
            }
        }
    }
}

我收到以下回复:

{
    "errors": [
        {
            "message": "Unknown argument \"products_PredominantColors_Contains\" on field \"patterns\" of type \"Query\".",
            "locations": [
                {
                    "line": 2,
                    "column": 24
                }
            ]
        }
    ]
}

我想,那是因为我没有使用DjangoFilterConnectionField 这里建议的,但是当我尝试这样做时:

class PatternConnection(relay.Connection):
    class Meta:
        node = PatternNode


class Query(graphene.ObjectType):
    pattern = relay.Node.Field(
        PatternNode,
        id=ID(),
        slug=String()
    )
    patterns = DjangoFilterConnectionField(PatternConnection)

我收到以下错误:

September 23, 2020 - 17:06:12
Django version 2.2.12, using settings 'proquinal_api.settings'
Starting development server at http://api.spradling.local:8000/
Quit the server with CONTROL-C.
/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/types.py:131: UserWarning: Django model "cities_light.City" does not have a field or attribute named "location". Consider removing the field from the "exclude" list of DjangoObjectType "CityNode" because it has no effect
  type_=type_,
Internal Server Error: /graphql/
Traceback (most recent call last):
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 79, in import_from_string
    module = importlib.import_module(module_path)
  File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/cristianrojas/www/spradling-api/proquinal_api/schema.py", line 49, in <module>
    mutation=Mutation
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/schema.py", line 78, in __init__
    self.build_typemap()
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/schema.py", line 168, in build_typemap
    initial_types, auto_camelcase=self.auto_camelcase, schema=self
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 80, in __init__
    super(TypeMap, self).__init__(types)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/typemap.py", line 31, in __init__
    self.update(reduce(self.reducer, types, OrderedDict()))  # type: ignore
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 88, in reducer
    return self.graphene_reducer(map, type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 117, in graphene_reducer
    return GraphQLTypeMap.reducer(map, internal_type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/typemap.py", line 109, in reducer
    field_map = type_.fields
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/pyutils/cached_property.py", line 22, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/definition.py", line 198, in fields
    return define_field_map(self, self._fields)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/definition.py", line 212, in define_field_map
    field_map = field_map()
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 275, in construct_fields_for_type
    map = self.reducer(map, field.type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/fields.py", line 98, in type
    assert _type._meta.connection, "The type {} doesn't have a connection".format(
AttributeError: 'ConnectionOptions' object has no attribute 'connection'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/views/generic/base.py", line 62, in view
    self = cls(**initkwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/views.py", line 100, in __init__
    schema = graphene_settings.SCHEMA
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 126, in __getattr__
    val = perform_import(val, attr)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 65, in perform_import
    return import_from_string(val, setting_name)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 88, in import_from_string
    raise ImportError(msg)
ImportError: Could not import 'proquinal_api.schema.schema' for Graphene setting 'SCHEMA'. AttributeError: 'ConnectionOptions' object has no attribute 'connection'.
[23/Sep/2020 17:08:02] "POST /graphql/ HTTP/1.1" 500 212017

所以我想知道DjangoFilterConnectionField与我PatternConnection的中继连接结合使用以使过滤器和分页一起工作的正确方法是什么。

标签: pythondjangographqlgraphene-django

解决方案


传递PatternNodeDjangoFilterConnectionFieldas

import graphene


class PatternNode(DjangoObjectType):
    # Many fields here...
    ...

    class Meta:
        model = Pattern
        interfaces = (relay.Node,)
        filterset_class = PatternFilterSet
        

class Query(graphene.ObjectType):
    patterns = DjangoFilterConnectionField(PatternNode)

推荐阅读