首页 > 解决方案 > 如何在某些字段中聚合具有相同值的多个对象?

问题描述

我有这个模型:

class Connection(models.Model):
    CONNECTION_CHOICES = [
        ('REST-API', 'REST-API'),
        ('SSH', 'SSH'),
        ('SFTP', 'SFTP'),
        ('SOAP-API', 'SOAP-API'),
    ]
    endpoint = models.CharField(max_length=240, blank=True, null=True)
    port = models.IntegerField(blank=True, null=True)
    connection_type = models.CharField(max_length=240, choices=CONNECTION_CHOICES)
    source_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='source-tool+')
    target_tool = models.ForeignKey(Tool, on_delete=models.CASCADE, related_name='target-tool+')


    def __str__(self):
        return self.source_tool.name + " to " + self.target_tool.name 
    

    def get_absolute_url(self):
        return reverse('tools:connection-detail', kwargs={'pk': self.pk})

在一个视图中,我正在尝试组合对象,其中 source_tool 和 target_tool 相同,但 connection_type 不同。

目前,我有这样的看法:

def api_map_view(request):
    json = {}
    nodes = []
    links = []
    connections = Connection.objects.all()

    for connection in connections:
        if {'name': connection.source_tool.name, 'id': connection.source_tool.id} not in nodes:
            nodes.append({'name': connection.source_tool.name, 'id': connection.source_tool.id})
        if {'name': connection.target_tool.name, 'id': connection.target_tool.id} not in nodes:
            nodes.append({'name': connection.target_tool.name, 'id': connection.target_tool.id})
        if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
            links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
        else:
            links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})
    json['nodes'] = nodes
    json['links'] = links
    print(json)
    return JsonResponse(data=json)

这返回,例如

{
   'nodes':
           [
               {'name': 'Ansible', 'id': 1 },
               {'name': 'Terraform', 'id': 2},
               {'name': 'Foreman', 'id': 3}
           ],
   'links':
           [
               {'source': 1, 'target': 2, 'type': 'SSH'},
               {'source': 2, 'target': 3, 'type': 'REST-API'}
               {'source': 1, 'target': 2, 'type': 'REST-API'}
           ]
}

我的用例是,我想修改连接,但我没有为同一连接获得 2 个不同的列表条目,只是类型不同。而不是上面的 JSON,我想实现这个:

{
   'nodes':
           [
               {'name': 'Ansible', 'id': 1 },
               {'name': 'Terraform', 'id': 2},
               {'name': 'Foreman', 'id': 3}
           ],
   'links':
           [
               {'source': 1, 'target': 2, 'type': 'SSH/REST-API'},
               {'source': 2, 'target': 3, 'type': 'REST-API'}
           ]
}

目前我无法创建查询或修改字典列表以查找条目,其中源和目标与当前条目相同(遍历列表),并修改类型字段。

我将 Django 3.1 与 Python 3.8 一起使用。

问候

标签: pythondjango

解决方案


问题出现在这些行中:

if {'source': connection.source_tool.id, 'target': connection.target_tool.id} in links:
    links.replace({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': links['type'] + '/' + connection_type})
else:
    links.append({'source': connection.source_tool.id, 'target': connection.target_tool.id, 'type': connection.connection_type})

您正在检查是否{'source': X, 'target': Y}在,links但对于第一次出现,您正在添加 {'source': X, 'target': Y, 'type': Z1}links. 所以你添加的项目永远不会True因为in links它有一个额外的键“类型”。

另一方面,您不能直接检查{'source': X, 'target': Y, 'type': Z1}in 链接,因为 then case when 不匹配'type': Z2

要解决此问题,请执行以下操作之一:


1.(首选)使用带有键的字典作为namedtuple源和目标的元组或只是一个元组。由于元组和命名元组是可散列的,它们可以用作字典键。

import collections  # at the top

links = {}  # links is now a dict, not a list
SourceTarget = collections.namedtuple('SourceTarget', 'source target')
# >>> SourceTarget('X', 'Y')  # to show how they work
# SourceTarget(source='X', target='Y')

像这样使用:

if (connection.source_tool.id, connection.target_tool.id) in links:  # tuples can match with namedtuples
    links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] += '/' + connection.connection_type
else:
    links[SourceTarget(connection.source_tool.id, connection.target_tool.id)] = connection.connection_type

最后,您希望它们作为对象/字典列表:

json['links'] = [{'source': st.source, 'target': st.target, 'type': type_}
                 for st, type_ in links.items()]
                # I used `type_` so that it doesn't conflict with Python own `type()`

2.(1 的变体)仍然需要使用元组或命名元组作为您的 dict 键,然后使用defaultdictwithlist来继续附加连接类型。

您不需要该if/else零件,您可以这样做:

import collections

links = collections.defaultdict(list)
...

# using tuples as the key instead of namedtuples....
links[(connection.source_tool.id, connection.target_tool.id)].append(connection.connection_type)

这将为每个条目创建新条目,(source, target)并将其type作为列表中的单个值,或者将附加到该列表中。无需if检查。

并将其转换为您的 json obj:

json['links'] = [{'source': st[0], 'target': st[1], 'type': '/'.join(types)}
                 for st, types) in links.items()]

顺便说一句,由于您使用的是Python 3.8,因此您可以使用赋值表达式,即“海象运算符”,以减少重复并使代码更简洁。

以选项 1 为例,它会使你的 if-block 的第一部分更加清晰,因为很明显你正在添加不存在的东西;无需阅读整条长线。

if (src := {'name': connection.source_tool.name, 'id': connection.source_tool.id}) not in nodes:
    nodes.append(src)
if (trg := {'name': connection.target_tool.name, 'id': connection.target_tool.id}) not in nodes:
    nodes.append(trg)
if (st := (connection.source_tool.id, connection.target_tool.id)) in links:
    # used a tuple to update _existing_ NT element
    links[st] += '/' + connection.connection_type
else:
    # but must use namedtuples when adding new elements
    links[SourceTarget(*st)] = connection.connection_type

推荐阅读