Skip to main content
 首页 » 编程设计

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

2024年10月01日37lonelyxmas

我有以下模型:

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 对象。通过这种方式,我可以追溯到特定 License 拥有的所有 Maintenance。

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

我知道我可以用一个 QuerySet 和一个 for 循环来实现这一点,但是这对服务器来说有点昂贵,因为有很多条目。

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

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

我知道我可以在某些情况下使用 __last,但如果我必须在之后指定一些内容,那将无法正常工作。

编辑

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

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') 

请您参考如下方法:

tl;dr )

当前重要的答案是末尾的 EDIT。

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

我认为您过于简化了您的真实模型,以至于效果不佳。 您有两个与匿名(非显式)关系表的链式 ManyToMany 关系。这使得编写正确的查询集变得不可能。

错误:

1) 您对字段及其 related_name 使用相同的名称(“licenses”和“maintenances”)。这是胡说八道,因为:docs

related_name
The name to use for the relation from the related object back to this one. It’s also the default value for related_query_name (the name to use for the reverse filter name from the target model).

在对象 Maintenance 上看到一个反向查询集 maintenances 是没有用的。类似地,关于客户许可的查询集“许可”。您可以轻松重命名 related_name,因为它不会更改数据库,也不会导致迁移。

2) License 是公共(public)对象还是单独对象?如果它是个人的,那么它不需要与 Customer 对象的多对多 关系。如果它很常见,那么您就无法通过它跟踪对个别客户的付费维护。 (您也不意味着两个客户是一个许可证的共同所有者!是吗?:-)您可能是指一个普通的 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) 
... )