Django:属性和查询集注释之间的逻辑重复 [英] Django: Duplicated logic between properties and queryset annotations

查看:29
本文介绍了Django:属性和查询集注释之间的逻辑重复的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我想定义自己的业务逻辑时,我一直在努力寻找正确的方法,因为我经常都需要一个属性和一个自定义查询集来获取相同的信息.最后,逻辑是重复的.

When I want to define my business logic, I'm struggling finding the right way to do this, because I often both need a property AND a custom queryset to get the same info. In the end, the logic is duplicated.

首先,在定义了类之后,我自然会为所需的数据编写一个简单的属性:

First, after defining my class, I naturally start writing a simple property for data I need:

class PickupTimeSlot(models.Model):

    @property
    def nb_bookings(self) -> int:
        """ How many times this time slot is booked? """ 
        return self.order_set.validated().count()

然后,我很快意识到,在处理查询集中的许多对象时调用此属性将导致重复的查询,并且会降低性能(即使我使用预取,因为会再次调用过滤).因此,我解决了编写带有注释的自定义查询集的问题:

Then, I quickly realise that calling this property while dealing with many objects in a queryset will lead to duplicated queries and will kill performance (even if I use prefetching, because filtering is called again). So I solve the problem writing a custom queryset with annotation:

class PickupTimeSlotQuerySet(query.QuerySet):

    def add_nb_bookings_data(self):
        return self.annotate(db_nb_bookings=Count('order', filter=Q(order__status=Order.VALIDATED)))

问题

然后,我遇到两个问题:

The issue

And then, I end up with 2 problems:

  • 我写了两次相同的业务逻辑("如何查找预订数量"),这可能导致功能错误.
  • 我需要找到两个不同的属性名称以避免冲突,因为显然,为属性和注释设置 nb_bookings 都不起作用.这迫使我在使用对象时考虑如何生成数据,调用正确的属性名称(例如 pickup_slot.nb_bookings (属性)或 pickup_slot.db_nb_bookings (注释))
  • I have the same business logic ("how to find the number of bookings") written twice, that could lead to functional errors.
  • I need to find two different attribute names to avoid conflicts, because obviously, setting nb_bookings for both the property and the annotation don't work. This forces me, when using my object, to think about how the data is generated, to call the right attribute name (let's say pickup_slot.nb_bookings (property) or pickup_slot.db_nb_bookings (annotation) )

对我来说,这似乎设计不好,我很确定有办法做得更好.我需要一种始终使用相同的业务逻辑来始终编写 pickup_slot.nb_bookings 并获得高效答案的方法.

This seems poorly designed to me, and I'm pretty sure there is a way to do better. I'd need a way to always write pickup_slot.nb_bookings and having a performant answer, always using the same business logic.

我当时正在考虑完全删除该属性,并且只嘲笑自定义查询集.然后,对于单个对象,将它们包装在查询集中只是为了能够在其上调用添加注释数据.像这样:

I was thinking of completely removing the property and keeking custom queryset only. Then, for single objects, wrapping them in querysets just to be able to call add annotation data on it. Something like:

pickup_slot = PickupTimeSlot.objects.add_nb_bookings_data().get(pk = pickup_slot.pk)

对我来说似乎很客气和不自然.你觉得呢?

Seems pretty hacky and unnatural to me. What do you think?

推荐答案

根据您的不同答案,我决定使用注释属性.我创建了一个缓存机制以使命名透明.主要优点是将业务逻辑仅保留在一个地方.我看到的唯一缺点是可以再次从数据库调用对象进行注释.IMO对性能的影响很小.

Based on your different good answers, I decided to stick with annotations and properties. I created a cache mechanism to make it transparent about the naming. The main advantage is to keep the business logic in one place only. The only drawback I see is that an object could be called from database a second time to be annotated. Performance impact stays minor IMO.

这是一个完整的示例,其中包含我模型中需要的3个不同属性.随时发表评论以改善这一点.

Here is a full example with 3 different attributes I need in my model. Feel free to comment to improve this.

class PickupTimeSlotQuerySet(query.QuerySet):

    def add_booking_data(self):
        return self \
            .prefetch_related('order_set') \
            .annotate(_nb_bookings=Count('order', filter=Q(order__status=Order.VALIDATED))) \
            .annotate(_nb_available_bookings=F('nb_max_bookings') - F('_nb_bookings')) \
            .annotate(_is_bookable=Case(When(_nb_bookings__lt=F('nb_max_bookings'),
                                             then=Value(True)),
                                        default=Value(False),
                                        output_field=BooleanField())
                      ) \
            .order_by('start')

class PickupTimeSlot(models.Model):
    objects = SafeDeleteManager.from_queryset(PickupTimeSlotQuerySet)()
   
    nb_max_bookings = models.PositiveSmallIntegerField()
    
    @annotate_to_property('add_booking_data', 'nb_bookings')
    def nb_bookings(self):
        pass
    
    @annotate_to_property('add_booking_data', 'nb_available_bookings')
    def nb_available_bookings(self):
        pass
    
    @annotate_to_property('add_booking_data', 'is_bookable')
    def is_bookable(self):
        pass

decorators.py

def annotate_to_property(queryset_method_name, key_name):
    """
    allow an annotated attribute to be used as property.
    """
    from django.apps import apps

    def decorator(func):
        def inner(self):
            attr = "_" + key_name
            if not hasattr(self, attr):
                klass = apps.get_model(self._meta.app_label,
                                       self._meta.object_name)
                to_eval = f"klass.objects.{queryset_method_name}().get(pk={self.pk}).{attr}"
                value = eval(to_eval, {'klass': klass})
                setattr(self, attr, value)

            return getattr(self, attr)

        return property(inner)

    return decorator

这篇关于Django:属性和查询集注释之间的逻辑重复的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆