Django ORM交叉积

3

我有三个模型:

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 对象?

我想避免在 Python 中使用循环。最好是一种能在数据库中解决问题的解决方案。

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


配置文件在哪里用到了? - Daniel Roseman
@DanielRoseman,我更新了问题。我希望现在更清楚了:对于每个客户和每种问题类型,数据库中都应该有一个配置(交叉乘积)。 - guettli
1个回答

2
如果你能够为每个问题类型承担一次数据库访问的成本,可以尝试以下未经测试的代码片段:
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())

这可能是可以接受的,除非您有很多问题类型,或者如果您每秒要执行此操作几次,但您也可以考虑设置一个明智的默认值,而不是使配置对象成为每种问题类型和客户组合的必需品(在我看来,这有点像设计上的问题)。
【更新】
我更新了问题:我想避免在Python中使用循环。最好是在数据库中解决此问题的解决方案。
在Django中,每个Queryset都是Model实例的列表或dict(values查询集),因此无法返回所需的格式(Model元组列表)而不进行一些Python处理(可能需要多次访问数据库)。最接近交叉积的方法是使用“extra”方法而没有where参数,但它涉及原始SQL并且需要知道其他模型的底层表名:
missing = Customer.objects.extra(
    select={"issue_type_id": 'appname_issuetype.id'},
    tables=['appname_issuetype']
)

因此,每个客户对象将具有额外的属性“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”参数中包含它们。
为了组装一个(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
           )
        """]
    )
]

我认为这种方法不仅需要与“生成器”版本相同数量的数据库访问,而且不够便携,难以阅读,并且违反了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]

总之,最好的情况是你只需要进行两个查询,但这可能会牺牲可读性和可移植性。与为每个客户和问题类型组合强制配置相比,拥有合理的默认设置可能是更好的设计。

这些都没有经过测试,如果还有一些问题需要解决,抱歉让你费心了。


我更新了问题:我想在Python中避免循环。最好是在数据库中解决这个问题的解决方案。 - guettli
1
恐怕你只能使用原始的 SQL 语句了,这已经超出了使用更方便的 ORM 方法一次完成的范围。 - Paulo Scardine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接