Django ORM 交叉产品

Django ORM Cross Product

我有三个模型:

class Customer(models.Model):
    pass

class IssueType(models.Model):
    pass

class IssueTypeConfigPerCustomer(models.Model):
    customer=models.ForeignKey(Customer)
    issue_type=models.ForeignKey(IssueType)
    class Meta:
        unique_together=[('customer', 'issue_type')]

如何在没有 IssueTypeConfigPerCustomer 对象的情况下找到 (customer, issue_type) 的所有元组?

我想避免 Python 中的循环。首选在数据库中解决此问题的解决方案。

背景:对于每个客户和每个问题类型,数据库中都应该有一个配置。

如果您有能力为每种问题类型进行一次数据库访问,请尝试类似以下未经测试的代码段:

def lacking_configs():
    for issue_type in IssueType.objects.all():
        for customer in Customer.objects.filter(
                issuetypeconfigpercustomer__issue_type=None
            ):
            yield customer, issue_type

missing = list(lacking_configs())

这可能没问题,除非你有很多问题类型或者如果你每秒这样做几次,但你也可以考虑有一个合理的默认值,而不是为每个问题类型的组合强制配置对象和客户(恕我直言,这有点设计味道)。

[更新]

I updated the question: I want to avoid a loop in Python. A solution which solves this in the DB would be preferred.

在 Django 中,每个 Queryset 要么是一个模型实例列表,要么是一个 dictvalues 查询集),所以不可能 return 你的格式想要(模型的 tuples 的列表)而没有一些 Python(并且可能多次访问数据库)。

最接近叉积的是使用不带 where 参数的 "extra" 方法,但它涉及原始 SQL 并知道基础 table 名称对于其他型号:

missing = Customer.objects.extra(
    select={"issue_type_id": 'appname_issuetype.id'},
    tables=['appname_issuetype']
)

因此,每个 Customer 对象都会有一个额外的属性 "issue_type_id",其中包含一个 IssueType 的 ID。您可以使用 where 参数根据 NOT EXISTS (SELECT 1 FROM appname_issuetypeconfigpercustomer WHERE issuetype_id=appname_issuetype.id AND customer_id=appname_customer.id) 进行过滤。使用 values 方法,您可以获得接近您想要的结果 - 这可能足以验证规则并创建丢失的记录。如果您需要 IssueType 的其他字段,只需将它们包含在 select 参数中即可。

为了 assemble (Customer, IssueType) 的列表,你需要像这样的东西:

cross_product = [
    (customer, IssueType.objects.get(pk=customer.issue_type_id))
    for customer in 
    Customer.objects.extra(
        select={"issue_type_id": 'appname_issuetype.id'},
        tables=['appname_issuetype'],
        where=["""
           NOT EXISTS (
               SELECT 1 
               FROM appname_issuetypeconfigpercustomer 
               WHERE issuetype_id=appname_issuetype.id 
                AND customer_id=appname_customer.id
           )
        """]
    )
]

不仅这需要与基于 "generator" 的版本相同的数据库访问次数,而且恕我直言,它在 table 方面也更少,可读性更差并且违反了 DRY。我猜你可以使用这样的方法将数据库查询的数量减少到几个:

missing = Customer.objects.extra(
    select={"issue_type_id": 'appname_issuetype.id'},
    tables=['appname_issuetype'],
    where=["""
       NOT EXISTS (
           SELECT 1 
           FROM appname_issuetypeconfigpercustomer 
           WHERE issuetype_id=appname_issuetype.id 
             AND customer_id=appname_customer.id
       )
    """]
)
issue_list = dict(
    (issue.id, issue)
    for issue in 
    IssueType.objects.filter(
        pk__in=set(m.issue_type_id for m in missing)
    )
)
cross_product = [(c, issue_list[c.issue_type_id]) for c in missing]

底线:在最好的情况下,您会以易读性和可移植性为代价进行两次查询。与 Customer 和 IssueType 的每个组合的强制配置相比,具有合理的默认值可能是更好的设计。

这都是未经测试的,抱歉,如果有一些功课留给你。