Search code examples
pythondjangoapidjango-rest-frameworkhttp-options-method

Disabling choices on rest_framework's RelatedField


I'm using django rest framework version 3.3.2.

We use HyperlinkedRelatedField in hundreds of different places, and my problem is that this inherits a choices method through RelatedField which does the following:

class RelatedField(Field):

    ...

    @property
    def choices(self):
        queryset = self.get_queryset()
        if queryset is None:
            # Ensure that field.choices returns something sensible
            # even when accessed with a read-only field.
            return {}

        return OrderedDict([
            (
                six.text_type(self.to_representation(item)),
                self.display_value(item)
            )
            for item in queryset
        ])

That queryset is a relation to another table, and can contains hundreds of thousands of rows. An OPTIONS request to the api now consumes all available memory, as it tries to generate the json response for the available choices of the relation. Even though html_cutoff option truncates this number to 1000, the issue remains because the queryset has already been consumed before it is limited by the cutoff.

I'm looking for a non-intrusive way to disable the choices enumeration on foreign keys. I would like to avoid creating a custom field class, if possible, is there a way to influence this behaviour through the rest framework api? I don't need to see the choices at all in the options response.


Solution

  • In current DRF (v3.3.2) there is this problem, OPTIONS response attempts to evaluate 'choices' for foreign keys. This is a terrible idea and makes the browsable API unusable if you have a non-trivial amount of content in your database.

    DRF maintainers are aware of the fact, and there is a PR currently scheduled for the 3.4.0 release to address the issue.

    Until it's fixed upstream, this is my workaround. Note: to override the behaviour, you'll need to set the DEFAULT_METADATA_CLASS under the REST_FRAMEWORK options in your settings.py.

    This intentionally doesn't disable the choices enumeration for ChoiceField and friends, only for related fields.

    from copy import copy
    from functools import wraps
    
    from rest_framework.metadata import SimpleMetadata
    from rest_framework.relations import RelatedField
    
    
    class MyMetadata(SimpleMetadata):
    
        def get_field_info(self, field):
    
            if isinstance(field, RelatedField):
                def kill_queryset(f):
                    @wraps(f)
                    def wrapped(*args, **kwargs):
                        qs = f(*args, **kwargs)
                        if qs is not None:
                            qs = qs.none()
                        return qs
                    return wrapped
    
                field = copy(field)
                field.get_queryset = kill_queryset(field.get_queryset)
    
            result = super(MyMetadata, self).get_field_info(field)
    
            if not result.get('choices'):
                result.pop('choices', None)