首页 > 解决方案 > 从 Django Admin 中的相关模型获取最新付款

问题描述

我想在 Django admin 中使用“前提”模型输出一个表。此外,我想在此表的附加列中包含输出,例如“最后一次公用事业付款”。它实际上是相关表中的一列。数据库中可能没有付款,因此管理员应该能够看到一个空单元格或付款​​日期。

我能够编写一个数据库查询来显示我需要的信息。其重要和工作部分如下:

SELECT jp.id,
       jp.number apartment,
       jp.building_number building,
       jp.rent,
       jp.arrears,
       jpm.last_payment
FROM jasmin_premises jp
LEFT JOIN (
        SELECT pm.premises_id,
               max(pm.paid) last_payment
        FROM jasmin_payment pm
        GROUP BY pm.premises_id
    ) jpm ON jp.id = jpm.premises_id;

并且输出类似于以下内容:

id  | apartment | building | rent   | arrears | last_payment
--------------------------------------------------------------
170 | 1X        | 6-A      | 297.43 | 2.57,   | NULL
72  | 2         | 4        | 289.66 | -678.38 | 2021-01-31
173 | 3Z        | 7        | 432.86 | 515.72  | 2021-02-04
73  | 4         | 8-B      | 292.25 | 515.44  | 2021-02-04
74  | 5         | 8-B      | 112.42 | 3249.34 | NULL
75  | 6A        | 122      | 328.48 | 386.23  | 2021-02-04
76  | 7         | 42       | 482.06 | 964.12  | 2021-01-31
77  | 8         | 1        | 433.71 | 867.42  | 2021-01-31
78  | 9C        | 12       | 322.79 | 322.79  | 2021-02-04
79  | 10        | 122      | 324.22 | 0       | 2021-02-04
80  | 12        | 12       | 322.79 | 1232.46 | NULL
81  | 14        | 5-Z      | 440.82 | 978.44  | 2021-02-04

我正在使用以下模型(仅是重要部分):

class Premises(models.Model):
    number = models.CharField(
        blank=False,
        null=False,
        max_length=10)

    building_number = models.CharField(
        blank=False,
        null=False,
        max_length=3)

    rent = models.DecimalField(
        blank=False,
        null=False,
        max_digits=12,
        decimal_places=2,
        default=0.0)

    area = models.DecimalField(
        blank=False,
        null=False,
        max_digits=5,
        decimal_places=2)

class Payment(models.Model):
    paid = models.DateField(
        blank=False,
        null=False)

    premises = models.ForeignKey(
        Premises,
        blank=False,
        null=False,
        on_delete=models.CASCADE,
        related_name='payments',
        db_index=True)

有没有办法覆盖admin.ModelAdmin.get_queryset(例如使用注释)来获得一个额外的列,就像我上面的例子一样?有没有其他方法可以使用 Django ORM 对复合数据库查询进行 LEFT JOIN?

标签: djangodjango-modelsdjango-admindjango-querysetdjango-orm

解决方案


解决方案是使用Subquery表达式向QuerySet添加显式子查询。我们还需要使用OuterRef,因为子查询中的查询集需要引用外部查询中的字段。

所以让我们创建一个子查询:

from django.db.models import OuterRef

payments = Payment.objects.filter(
    premises=OuterRef('pk')
).order_by('-paid')

下一步是将子查询传递给payments查询集:

from django.db.models import Subquery

# 'payments' here is from example above
premises = Premises.objects.annotate(
    last_payment=Subquery(payments.values('paid')[:1])
)

最后,让我们看看使用 SQL 来查询数据库中的对象行:

print(premises.query)

(输出已格式化,仅显示重要部分)

SELECT "jasmin_premises"."id",
       "jasmin_premises"."number",
       "jasmin_premises"."building_number",
       "jasmin_premises"."arrears",
       "jasmin_premises"."rent",
       (SELECT U0."paid"
        FROM "jasmin_payment" U0
        WHERE U0."premises_id" = "jasmin_premises"."id"
        ORDER BY U0."paid" DESC
        LIMIT 1) AS "last_payment"
FROM "jasmin_premises";

现在,在执行测试之后,我们可以在 ModelAdmin 中使用它:

from django.contrib import admin
from django.db.models import OuterRef, Subquery

from .models import Payment, Premises


@admin.register(Premises)
class PremisesAdmin(admin.ModelAdmin):

    list_display = (
        'number',
        'building_number',
        'rent',
        'arrears',
        'last_payment',
    )

    def get_queryset(self, request):
        qs = super().get_queryset(request)

        payments = Payment.objects.filter(
            premises=OuterRef('pk')
        ).order_by('-paid')

        qs = qs.annotate(
            last_payment=Subquery(payments.values('paid')[:1]),
        )

        return qs

    def last_payment(self, obj):
        return obj.last_payment
    last_payment.short_description = 'Last payment'
    last_payment.admin_order_field = 'last_payment'

好吧,这不使用 JOIN,但这种方法将强制 Django 执行子查询。

可能在某些情况下,可以编写一个等效的查询集来更清晰或更有效地执行相同的任务,但是,这是我迄今为止取得的最好成绩。


推荐阅读