Django: Force select related?

前端 未结 3 1629
情歌与酒
情歌与酒 2021-02-01 02:09

I\'ve created a model, and I\'m rendering the default/unmodified model form for it. This alone generates 64 SQL queries because it has quite a few foreign keys, and those in tur

3条回答
  •  余生分开走
    2021-02-01 02:51

    Here's also a fun trick:

    class DefaultSelectOrPrefetchManager(models.Manager):
        def __init__(self, *args, **kwargs):
            self._select_related = kwargs.pop('select_related', None)
            self._prefetch_related = kwargs.pop('prefetch_related', None)
    
            super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
    
        def get_queryset(self, *args, **kwargs):
            qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
    
            if self._select_related:
                qs = qs.select_related(*self._select_related)
            if self._prefetch_related:
                qs = qs.prefetch_related(*self._prefetch_related)
    
            return qs
    
    
    class Sandwich(models.Model):
        bread = models.ForeignKey(Bread)
        extras = models.ManyToManyField(Extra)
    
        # ...
    
        objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))
    

    Then you can re-use the manager easily between model classes. As an example use case, this would be appropriate if you had a __unicode__ method on the model which rendered a string that included some information from a related model (or anything else that meant a related model was almost always required).

    ...and if you really want to get wacky, here's a more generalized version. It allows you to call any sequence of methods on the default queryset with any combination of args or kwargs. There might be some errors in the code, but you get the idea.

    from django.db import models
    
    
    class MethodCalls(object):
        """
        A mock object which logs chained method calls.
        """
        def __init__(self):
            self._calls = []
    
        def __getattr__(self, name):
            c = Call(self, name)
            self._calls.append(c)
            return c
    
        def __iter__(self):
            for c in self._calls:
                yield tuple(c)
    
    
    class Call(object):
        """
        Used by `MethodCalls` objects internally to represent chained method calls.
        """
        def __init__(self, calls_obj, method_name):
            self._calls = calls_obj
            self.method_name = method_name
    
        def __call__(self, *method_args, **method_kwargs):
            self.method_args = method_args
            self.method_kwargs = method_kwargs
    
            return self._calls
    
        def __iter__(self):
            yield self.method_name
            yield self.method_args
            yield self.method_kwargs
    
    
    class DefaultQuerysetMethodCallsManager(models.Manager):
        """
        A model manager class which allows specification of a sequence of
        method calls to be applied by default to base querysets.
        `DefaultQuerysetMethodCallsManager` instances expose a property
        `default_queryset_method_calls` to which chained method calls can be
        applied to indicate which methods should be called on base querysets.
        """
        def __init__(self, *args, **kwargs):
            self.default_queryset_method_calls = MethodCalls()
    
            super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)
    
        def get_queryset(self, *args, **kwargs):
            qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)
    
            for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
                qs = getattr(qs, method_name)(*method_args, **method_kwargs)
    
            return qs
    
    
    class Sandwich(models.Model):
        bread = models.ForeignKey(Bread)
        extras = models.ManyToManyField(Extra)
    
        # Other field definitions...
    
        objects = DefaultQuerysetMethodCallsManager()
        objects.default_queryset_method_calls.filter(
            bread__type='wheat',
        ).select_related(
            'bread',
        ).prefetch_related(
            'extras',
        )
    

    The python-mock-inspired MethodCalls object is an attempt at making the API more natural. Some might find that a bit confusing. If so, you could sub out that code for an __init__ arg or kwarg that just accepts a tuple of method call information.

提交回复
热议问题