如何使用Django中的QuerySet过滤最新的m2m对象

时间:2017-02-28 10:03:01

标签: python django

我有以下型号:

class Customer(SomeInheritedModel):
    name = models.CharField(max_length=50)
    ...

class Account(SomeInheritedModel):
    customer = models.ForeignKey(Customer, related_name='accounts')
    ...

class Product(SomeInheritedModel):
    name = models.CharField(max_length=50)
    ...

class License(SomeInheritedModel):
    account = models.ForeignKey(Account)
    product = models.ForeignKey(Product)
    maintenance = models.ManyToManyField('Maintenance', related_name="maintenances")

class Maintenance(SomeInheritedModel):
    start_date = models.DateTimeField(null=True)
    expiration_date = models.DateTimeField(null=True)

续订许可证维护后,将创建一个新的Maintenance对象。通过这种方式,我可以追溯到特定Maintenance所拥有的所有License

现在,我想根据他们的Customers到期日生成一份报告,向我展示License即将过期的所有Maintenance。我只想要一个Maintenance最新 License对象,因为它是最新出售的。我不想要其他人。

我知道我可以使用QuerySet和for循环来实现这一点,但是如果有很多条目,这对服务器来说会有点麻烦。

有没有办法通过QuerySet进行过滤?像这样:

Customer.objects.filter(accounts__licenses__maintenances__expiry_date__last__range=(now().date(), one_month_into_future().date()))

我知道我可以在某些情况下使用__last,但是如果我必须在那之后指定一些内容,那就不行了。

修改

我通过@hynekcer的建议找到了答案。您可以使用annotate

License.objects.filter(foo=True)
    .annotate(max_exp_date=models.Max('maintenances__expiration_date'))\
    .filter(max_exp_date__gte=report.start_date, max_exp_date__lte=report.end_date)\
    .select_related('account__customer')

2 个答案:

答案 0 :(得分:1)

在这种情况下,您有两种选择: 首先是使用prefetch_related:

from django.db.models import Prefetch

now = timezone.now()
maintenance_qs = Maintenance.objects.filter(expiry_date__lte=now).order_by('-expire_date')
license_qs = License.objects.filter(maintenances__expiry_date__lte=now).\
    prefetch_related(
        Prefetch('maintenances', queryset=maintenance_qs)
    ).order_by(-'maintenances__expiry_date')
customers = Customer.objects.prefetch_related(Prefetch('licenses', queryset=license_qs))

它会点击数据库3次,您可以阅读有关prefetch_relatedPrefetch object的更多信息。它将返回所有许可证和所有维护,但它将被排序,您只能采取1项。你可以像这样使用它。

for customer in customers:
    last_license = customer.licenses.all()[0]
    last_maintenance = last_license.maintenances.all()[0]

或者您可以尝试使用原始SQL。您的查询如下:

customers = Customer.objects.raw(
'''
SELECT * FROM (
    SELECT "yourapp_customer"."id", 
           "yourapp_license"."id", 
           "yourapp_maintenance"."id",
           "yourapp_maintanance"."start_date",
           "yourapp_maintanance"."expiration_date",
           MAX("yourapp_maintanance"."expiration_date") over (partition by "yourapp_customer"."id") as last_expired
    FROM "yourapp_customer"
    LEFT OUTER JOIN "yourapp_customer_licenses" ON
        "yourapp_customer"."id" = "yourapp_customer_licenses"."customer_id"
    LEFT OUTER JOIN "yourapp_license" ON
        "yourapp_license"."id" = "yourapp_customer_licenses"."license_id"
    LEFT OUTER JOIN "yourapp_license_maintenances" ON
        "yourapp_license"."id" = "yourapp_license_maintenances"."license_id"
    LEFT OUTER JOIN "yourapp_maintanance" ON
        "yourapp_maintanance"."id" = "yourapp_license_maintenances"."maintanance_id"
    WHERE "yourapp_maintanance"."expiration_date" < NOW()
) AS T
where expiration_date = last_expired
'''
)

它应该更快,但使用此查询您无法构建许可证和维护对象。所有属性都将存储在Customer模型中。您可以阅读有关window functions

的更多信息

答案 1 :(得分:1)

tl; dr)

目前重要的答案是最后的编辑。

是的,重要的是忽略所有旧的相关对象(维护),即使那些仍然有效的对象,因为可能存在重新维护。

我认为你如此简化了你的真实模型,它并没有很好地运作。 您与匿名(非显式)关系表有两个链接的ManyToMany关系。这使得无法编写正确的查询集。

<强>错误:

1)您对字段及其related_name使用相同的名称(&#34;许可证&#34;以及&#34;维护&#34;)。这是胡说八道,因为:docs

  

<强> related_name
  用于从相关对象返回到此关系的关系的名称。它也是related_query_name的默认值(用于目标模型的反向过滤器名称的名称)。

在对象maintenances上看到反向查询集Maintenance到许可证是没用的。类似地,查询集'license&#39;关于客户许可。您可以轻松地重命名related_name,因为它不会更改数据库并且不会导致迁移。

2)License是一个共同的或个别的对象吗?如果是个人,那么它与Customer对象之间不需要many-to-many关系。如果这种情况很常见,那么您无法通过它跟踪个人客户的付费维护。 (你也不是说两个客户是一个许可证的共同拥有者!是吗?:-)你可能意味着一个共同的LicensedProduct和一个个人License将客户与产品。我知道用户可以购买一个维护更多的许可证,并且多对多在这里很好。

首先我修复了模型(在我问你之前我想的某种方式)

class Customer(SomeInheritedModel):
    # "licenses" is the reverse query to License
    # optionally you can enable many-to-many relation to licensed products
    # lic_products = models.ManyToManyField(

class Product(models.Model):
    pass  # licensed product details

class License(SomeInheritedModel):
    customer = models.ForeignKey(Customer, related_name='licenses')
    product = models.ForeignKey(Product, related_name='+')  # used '+' because not deeded
    maintenances = models.ManyToManyField(
        Maintenance,
        through='LicenseMaintenance',
        through_fields=('license', 'maintenance'),
        related_name='licenses')

class Maintenance(SomeInheritedModel):
    start_date = DateTimeField(null=True)
    expiration_date = DateTimeField(null=True)

class LicenseMaintenance(models.Model):
    license = models.ForeignKey(License, on_delete=models.CASCADE)
    maintenance = models.ForeignKey(Maintenance, on_delete=models.CASCADE)

querysets :(可以通过删除order_by和相关字段来简化)

remind_start = datetime.datetime.now(tz=TIMEZONE)
remind_end = remind_start + datetime.timedelta(days=30)

expiring_lic_maintenances = (
    LicenseMaintenance.objects.values('license',
                                      'license__customer',
                                      'license__customer__name')
    .annotate(max_exp_date=models.Max('maintenance__expiration_date'))
    .filter(max_exp_date__lte=remind_start, max_exp_date__gte=remind_end)
    .order_by('license__customer__name', 'license__customer', 'license')
)   # some small detail can be used like e.g. customer name in the example, not used later

expiring_licenses = (
    License.objects.filter(
        license__in=expiring_lic_maintenances.values_list('license', flat=True))
    .select_related('customer', 'product')
    .order_by('license__customer__name', 'license__customer', 'license')
)   # that queryset with subquery is executed by one SQL command

通过运行这些查询集执行的SQL请求不超过两个:

# but I prefer a simple map and queryset with subquery:
expiration_map = {x.license_id: x.max_exp_date for x in expiring_lic_maintenances}


for lic in expiring_licenses:
    print("{name}, your maintenance for {lic_name} is expiring on {exp_date}".format(
        name=lic.customer.name,
        lic_name=lic.product.name,
        exp_date=expiration_map[lic.id],
    ))

我希望,这是一个新项目,您还不需要迁移修改后的模型。我写了很多次类似的代码,以至于我现在还没有验证它。可能会发生错误,您可以在赏金结束前通知我足够的时间。

编辑问题后编辑:
聚合函数在多对多字段中正常工作,而没有当前Django版本中连接表的显式模型:

>>> expiring = (
...     License.objects.values('id',
...                            'account__customer',
...                            'account__customer__name')
...     .annotate(max_exp_date=models.Max('maintenance__expiration_date'))
...     .filter(max_exp_date__gte=remind_start, max_exp_date__lte=remind_end)
... )

并查看编译的SQL

>>> str(expiring.query)
SELECT app_license.id, app_account.customer_id, app_customer.name, MAX(app_maintenance.expiration_date) AS max_exp_date
    FROM app_license INNER JOIN app_account ON (app_license.account_id = app_account.id)
    INNER JOIN app_customer ON (app_account.customer_id = app_customer.id)
    LEFT OUTER JOIN app_license_maintenance ON (app_license.id = app_license_maintenance.license_id)
    LEFT OUTER JOIN app_maintenance ON (app_license_maintenance.maintenance_id = app_maintenance.id)
    GROUP BY app_license.id, app_account.customer_id, app_customer.name
    HAVING (MAX(app_maintenance.expiration_date) >= 2017-04-07T13:45:35.485755 AND
            MAX(app_maintenance.expiration_date) <= 2017-03-08T13:45:35.485755
            )

通常,这是由两个外连接编译的。

如果您发现一个更复杂的案例,它不起作用或查询速度慢,因为某些数据库引擎使用外连接进行优化会更复杂,您可以每次都获得隐式模型并对其运行查询,因为它是关系层次结构中的顶级模型:

我们可以探索表格的隐式中间模型

>>> License.maintenance.through
app.models.License_maintenance
>>> LicenseMaintenance = License.maintenance.through
>>> LicenseMaintenance._meta.fields
(<django.db.models.fields.AutoField: id>,
 <django.db.models.fields.related.ForeignKey: license>,
 <django.db.models.fields.related.ForeignKey: maintenance>)

使用 :(所有连接都自动编译为内部连接)

>>> expiring = (
...     LicenseMaintenance.objects.values('license',
...                                       'license__account__customer',
...                                       'license__account__customer__name')
...     .annotate(max_exp_date=models.Max('maintenance__expiration_date'))
...     .filter(max_exp_date__lte=remind_start, max_exp_date__gte=remind_end)
... )