我有以下型号:
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')
答案 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_related和Prefetch 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)
目前重要的答案是最后的编辑。
是的,重要的是忽略所有旧的相关对象(维护),即使那些仍然有效的对象,因为可能存在重新维护。
我认为你如此简化了你的真实模型,它并没有很好地运作。 您与匿名(非显式)关系表有两个链接的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)
... )