Django:属性和查询集注释之间的重复逻辑

Dav*_* D. 14 python django django-models django-queryset

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

让我解释...

首先,在定义我的类之后,我自然而然地开始为我需要的数据编写一个简单的属性:

class PickupTimeSlot(models.Model):

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

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

class PickupTimeSlotQuerySet(query.QuerySet):

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

问题

然后,我最终遇到了两个问题:

  • 我写了两次相同的业务逻辑(“如何查找预订数量”),这可能会导致功能错误。
  • 我需要找到两个不同的属性名称以避免冲突,因为显然,nb_bookings属性和注释的设置都不起作用。这迫使我在使用我的对象时考虑数据是如何生成的,调用正确的属性名称(比如说pickup_slot.nb_bookings(property) 或pickup_slot.db_nb_bookings(annotation) )

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

我有一个想法,但我不确定...

我正在考虑完全删除该属性并仅保留自定义查询集。然后,对于单个对象,将它们包装在查询集中只是为了能够在其上调用添加注释数据。就像是:

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

对我来说似乎很hacky和不自然。你怎么认为?

Sar*_*iev 7

我不认为这里有灵丹妙药。但是对于这种情况,我在我的项目中使用了这种模式。

class PickupTimeSlotAnnotatedManager(models.Manager):
    def with_nb_bookings(self):
        return self.annotate(
            _nb_bookings=Count(
                'order', filter=Q(order__status=Order.VALIDATED)
            )
        )

class PickupTimeSlot(models.Model):
    ...
    annotated = PickupTimeSlotAnnotatedManager()

    @property
    def nb_bookings(self) -> int:
        """ How many times this time slot is booked? """ 
        if hasattr(self, '_nb_bookings'):
            return self._nb_bookings
        return self.order_set.validated().count()
Run Code Online (Sandbox Code Playgroud)

在代码中

qs = PickupTimeSlot.annotated.with_nb_bookings()
for item in qs:
    print(item.nb_bookings)
Run Code Online (Sandbox Code Playgroud)

这样我总是可以使用属性,如果它是带注释的查询集的一部分,它将使用带注释的值,否则它将计算它。这种方法保证我将通过使用所需的值对其进行注释来完全控制何时使查询集“更重”。如果我不需要这个,我就用普通的PickupTimeSlot.objects. ...

此外,如果有很多这样的属性,您可以编写装饰器来包装属性并简化代码。它将作为cached_property装饰器工作,但如果存在,它将使用带注释的值。


Dav*_* D. 1

根据您不同的良好答案,我决定坚持使用注释属性。我创建了一个缓存机制来使其命名透明。主要优点是将业务逻辑仅保留在一处。我看到的唯一缺点是可以从数据库中第二次调用对象来进行注释。在我看来,性能影响仍然很小。

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

模型.py

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
Run Code Online (Sandbox Code Playgroud)

装饰器.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
Run Code Online (Sandbox Code Playgroud)