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.
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)