0

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.

wim
  • 338,267
  • 99
  • 616
  • 750

2 Answers2

3

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)
wim
  • 338,267
  • 99
  • 616
  • 750
0

You can modify the content of any OPTIONS request in Django REST framework using the Metadata API.

This involves defining your own Metadata class - see this documentation page.

You can add your custom metadata class to the view that is causing issues.

Kieran
  • 2,554
  • 3
  • 26
  • 38
  • Indeed, I'm looking at a custom metadata class as probably the best place to attack this. But there doesn't seem to be any appropriate method to override for it, the last chance before it goes to the serializer and fields is in `get_field_info` (which does a lot of other essential stuff). – wim Feb 22 '16 at 22:27
  • @wim I created a new Metadata class that overrode `get_field_info`. I started with the `SimpleMetadata.get_field_info` as a template. I know this isn't super clean but it's the only way I found! – Kieran Feb 22 '16 at 22:39
  • If you use the `SimpleMetadata.get_field_info` then you have already consumed the queryset, so that's no good .. – wim Feb 22 '16 at 23:21
  • @wim That's annoying, I hadn't thought about that. I will let you know if I find an answer, but I need to know too! – Kieran Feb 24 '16 at 22:53
  • 1
    Hi Kieran, I've implemented a workaround using the metadata API. It's pretty hacky, because it's actually `RelatedField.choices` property which causes the issue, so you have to neuter the queryset before the field gets any chance to evaluate it. See my answer for the gory details. – wim Feb 26 '16 at 21:51