【问题标题】:Django: Force select related?Django:强制选择相关?
【发布时间】:2011-06-21 00:48:46
【问题描述】:

我已经创建了一个模型,并且正在为它呈现默认/未修改的模型表单。仅此一项就生成了 64 个 SQL 查询,因为它有相当多的外键,而这些外键又具有更多的外键。

是否可以强制它总是(默认情况下)每次返回这些模型之一时执行select_related

【问题讨论】:

    标签: django django-orm


    【解决方案1】:

    这里还有一个有趣的技巧:

    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',))
    

    然后您可以轻松地在模型类之间重用管理器。作为一个示例用例,如果您在模型上有一个 __unicode__ 方法,该方法呈现一个包含来自相关模型的一些信息的字符串(或任何其他意味着相关模型几乎总是 em> 必需)。

    ...如果您真的想变得古怪,这里有一个更通用的版本。它允许您使用argskwargs 的任意组合调用默认查询集上的任何方法序列。代码中可能有一些错误,但你明白了。

    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',
        )
    

    受python-mock 启发的MethodCalls 对象是一种使API 更自然的尝试。有些人可能会觉得这有点令人困惑。如果是这样,您可以将该代码替换为仅接受方法调用信息元组的 __init__ arg 或 kwarg。

    【讨论】:

    • 这真的拯救了我的一天——与其写成吨的ModelForms,不如覆盖ModelChoiceFields,直到奶牛回家。 (特别是如果MyModel.__unicode__() 使用需要select_related 的内容。
    • 很好的答案。这解决了我的大部分 n+1 相关问题
    • 注意:get_query_setdeprecated in Django 1.6。现在应该替换为get_queryset
    【解决方案2】:

    您可以创建一个自定义管理器,然后简单地覆盖 get_queryset 以使其适用于任何地方。例如:

    class MyManager(models.Manager):
        def get_queryset(self):
            return super(MyManager, self).get_queryset().select_related('foo', 'bar')
    

    (在 Django 1.6 之前,它是get_query_set)。

    【讨论】:

    • 我们在使用models.QuerySet时如何做到这一点?
    • 我认为您需要提供更多信息或一些示例代码来说明您的意思;如果你有一个 QuerySet,那么你可以直接在它上面调用select_related
    • as_manager 返回一个初始管理器对象; docs.djangoproject.com/en/1.8/topics/db/managers/#from-queryset 的文档解释了如何使用自定义管理器和自定义查询集。你应该能够像上面那样定义你的经理,定义你的自定义查询集,然后使用类似objects = MyManager.from_queryset(MyQueryset)()
    【解决方案3】:

    创建一个自定义models.Manager 并覆盖所有方法(filterget 等)并将 select_related 附加到每个查询中。然后将此管理器设置为模型上的objects 属性。

    我建议只检查您的代码并在需要的地方添加 select_related,因为对所有内容执行 select_related 会导致一些严重的性能问题(而且它的来源并不完全清楚) .

    【讨论】:

    • 然后我必须覆盖模型表单中的所有表单字段,以便我可以手动设置查询集...
    • 好吧,你总是可以接受我的第一个建议。我只是警告您可能会遇到的有害性能问题。有一些方法可以仅针对ModelForm 而不覆盖所有内容,但答案实际上取决于您到底需要做什么。如果您需要帮助,只需创建另一个包含更多详细信息的问题。
    • 嗯,我有一个地址模型,其中包含与邮政编码、城市、省和国家/地区的链接。如果没有这些字段,它几乎永远不会显示,所以我想我不妨默认包含它。
    • 只需在Address 模型上添加我建议的自定义管理器,就可以了。
    • @Izzad-DinRuhulessin 已经好几年了,但我想我是用它们来查找城市/省/国家/地区——它们不是更大地址的一部分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 2020-05-09
    • 1970-01-01
    • 1970-01-01
    • 2016-12-11
    相关资源
    最近更新 更多