【问题标题】:Django: retrieve single object from ManyToMany relation effectivelyDjango:有效地从多对多关系中检索单个对象
【发布时间】:2017-05-03 05:55:53
【问题描述】:

我有一个数据库结构,可以简化如下(版本和附加信息未显示,因为与我的问题没有直接关系):

class Image(models.Model):    
    file = models.ImageField(blank=False, null=False, upload_to='images')
    version = models.ForeignKey(Version, blank=False, null=False)

class ExampleImage(models.Model):
    example = models.ForeignKey('Example', blank=False, null=False)
    image = models.ForeignKey(Image, blank=False, null=False)
    additional_info = models.ForeignKey(AdditionalInfo, blank=True, null=True)

class Example(models.Model):
    name = models.Charfield(max_length=255)
    images = models.ManyToManyField(Image, through=ExampleImage, through_fields=('example', 'image')

    def get_default_image(self):
        try:
            image = self.images.get(version=Version.objects.get(name=DEFAULT_VERSION))
        except Image.DoesNotExist:
             # Get some other image, this happens maybe 1 out of 10

现在我有一个非常常见的需求,即查询所有 Example 对象及其默认图像。目前,我正在做以下事情:

example_list = []
examples = Example.objects.all()

for example in examples:
    example_dict = dict(name=example.name, image=example.get_default_image())
    example_list.append(example_dict)

# Then show example_list in template

这种方法有效,但它会导致数千个数据库查询,并且需要一分钟以上的时间来执行,这对于下载单个网页来说太长了!

所以我的问题是,优化这种用例的正确方法是什么。我可以为 Example 模型设置 default_image 字段(然后使用 select_related,但这会使添加图像更加复杂),将版本 ID 硬编码为 get_default_image 方法(不太灵活),将 get_default_image 方法设置为 cached_property 等,但我不完全确定应该使用哪种方法。我已经尝试了很多技巧,但似乎对我的情况没有任何帮助。我应该找到一种解决方案,它允许我用一个大查询同时查询示例和默认图像,而不是在 for 循环中进行工作。

【问题讨论】:

    标签: django django-models django-queryset database-optimization


    【解决方案1】:

    DEFAULT_VERSION 是模块常量吗?如果是这样,您可以排除version=Version.objects.get(name=DEFAULT_VERSION) 调用,这样您就不必为每个example in examples 执行它。

    您还应该能够使用自定义 Prefetch 对象:https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.Prefetch。与此类似的东西(尽管您必须自己测试和调整):

    qs = Image.filter(version=Version.objects.get(name=DEFAULT_VERSION))
    pref = Prefetch('images', queryset=qs, to_attr='image')
    examples = Example.objects.prefetch_related(pref)
    

    当然,理想情况下您应该将Example.default_image 设置为OneToOneField,这样您就可以有效地检索默认图像。

    cached_property 装饰器在这里不会真正帮助您,因为它只会在实例存在时缓存属性。如果您需要多次调用instance.get_default_image(),它确实有帮助,但您仍然需要为您获取的每个实例执行一次get_default_image

    希望有帮助

    【讨论】:

    • 是的,DEFAULT_VERSION 是一个模块常量。根据我的理解,Django 应该只为第一个示例访问数据库,并为其余示例使用缓存结果?我将在今天晚些时候检查自定义 Prefetch,感谢您的帮助!
    • Django 很聪明,但不是 那个 聪明。您在每个模型上调用get_default_image,该方法每次执行时都会从数据库中获取一个对象。有查询集缓存,但仅适用于 QS 的相同查询集 instance,例如在迭代examples 之后,只要您在同一范围内,调用examples[0] 就不会再次访问数据库。 Model.objects.get() 将始终尝试从数据库中获取对象。
    • 您的方法确实有效,我现在可以在 4 个查询中完成!但是有一些问题。首先,'image' 是一个列表,所以我需要在模板中访问它'image.0.file.url'。另一个问题是每个对象都不存在默认图像,在这种情况下,应该检索一些备用图像(请参阅我的 get_default_image 实现)。我目前的方法没有做到这一点。
    • 好吧,由于预取,图像将成为查询集。也许将to_attr='image' 更改为to_attr=images_pref 以明确这一点。至于默认图片,只需要写一个使用images_pref的模型方法,例如return getattr(self, 'images_pref', [DEFAULT_IMAGE])[0]
    猜你喜欢
    • 1970-01-01
    • 2018-05-23
    • 2021-04-06
    • 1970-01-01
    • 2015-01-06
    • 1970-01-01
    • 2019-11-27
    • 2011-09-14
    • 1970-01-01
    相关资源
    最近更新 更多