使用 Django ORM (CROSS JOIN) 计算组合

Lin*_*lle 5 django django-orm django-queryset

我有三个相关的模型:Process,FactorLevel. AProcessFactors 存在多对多关系,aFactor将拥有一个或多个Levels。我正在尝试计算Level与 a 相关的s 的所有组合Process。这很容易用 Pythonitertools作为模型方法来实现,但执行速度有点慢,所以我试图弄清楚如何使用 Django ORM 在 SQL 中执行这个计算。

楷模:

class Process(models.Model):
    factors = models.ManyToManyField(Factor, blank = True)

class Factor(models.Model):
    ...

class Level(models.Model):
    factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
Run Code Online (Sandbox Code Playgroud)

示例:一个过程'Running'涉及三个Factors ( 'Distance', 'Climb', 'Surface'),每个s由多个Levels ( 'Long'/ 'Short', 'Flat'/ 'Hilly', 'Road'/ 'Mixed'/ 'Trail') 组成。计算 SQL 中的组合将涉及通过首先确定Factor涉及多少s(在此示例中为 3)并CROSS JOIN多次执行所有级别的a来构建查询。

在 SQL 中,这可以这样完成:

WITH foo AS
    (SELECT * FROM Level
     WHERE Level.factor_id IN
        (SELECT ProcessFactors.factor_id FROM ProcessFactors WHERE process_id = 1)
    )
SELECT a1.*, a2.*, a3.*
    FROM foo a1
    CROSS JOIN foo a2
    CROSS JOIN foo a3
WHERE (a1.factor_id < a2.factor_id) AND (a2.factor_id < a3.factor_id)
Run Code Online (Sandbox Code Playgroud)

结果:

a1.name | a2.name | a3.name
--------------------------
Long    | Flat    | Road
Long    | Flat    | Mixed
Long    | Flat    | Trail
Long    | Hilly   | Road
Long    | Hilly   | Mixed
Long    | Hilly   | Trail
Short   | Flat    | Road
Short   | Flat    | Mixed
Short   | Flat    | Trail
Short   | Hilly   | Road
Short   | Hilly   | Mixed
Short   | Hilly   | Trail
Run Code Online (Sandbox Code Playgroud)

目前,我将此作为Process模型上的方法实现为:

a1.name | a2.name | a3.name
--------------------------
Long    | Flat    | Road
Long    | Flat    | Mixed
Long    | Flat    | Trail
Long    | Hilly   | Road
Long    | Hilly   | Mixed
Long    | Hilly   | Trail
Short   | Flat    | Road
Short   | Flat    | Mixed
Short   | Flat    | Trail
Short   | Hilly   | Road
Short   | Hilly   | Mixed
Short   | Hilly   | Trail
Run Code Online (Sandbox Code Playgroud)

这是否可以使用 Django ORM 或者它是否足够复杂以至于它应该作为原始查询实现以提高 Python 代码实现的速度?

几年前有一个关于在 Django ORM 中执行CROSS JOIN的类似问题(看起来像 Django v1.3)似乎并没有引起太多关注(作者试图只使用 Python itertools)。

vsd*_*vsd 3

from itertools import groupby, product
    
def level_combinations(self):
    # We need order by factor_id for proper grouping
    levels = Level.objects.filter(factor__process=self).order_by('factor_id')
    # [{'name': 'Long', 'factor_id': 1, ...},
    #  {'name': 'Short', 'factor_id': 1, ...},
    #  {'name': 'Flat', 'factor_id': 2, ...},
    #  {'name': 'Hilly', 'factor_id': 2, ...}]

    groups = [list(group) for _, group in groupby(levels, lambda l: l.factor_id)]
    # [[{'name': 'Long', 'factor_id': 1, ...},
    #   {'name': 'Short', 'factor_id': 1, ...}],
    #  [{'name': 'Flat', 'factor_id': 2, ...},
    #   {'name': 'Hilly', 'factor_id': 2, ...}]]

    # Note: don't forget, that product is iterator/generator, not list
    return product(*groups)
Run Code Online (Sandbox Code Playgroud)

如果顺序不重要,那么:

def level_combinations(self):
    levels = Level.objects.filter(factor__process=self)
    groups = {}
    for level in levels:
        groups.setdefault(level.factor_id, []).append(level)
    return product(*groups.values())
Run Code Online (Sandbox Code Playgroud)