我有以下模型:
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)
... )
