Django使用“通过”关系表从模型中获取所有相关对象

Posted

技术标签:

【中文标题】Django使用“通过”关系表从模型中获取所有相关对象【英文标题】:Django get all related objects from model with 'through' relationship tables 【发布时间】:2017-05-26 17:04:53 【问题描述】:

在我们的应用程序中,我们有几个关系和几个模型,我试图实现一种通用的方法来获取一个对象的所有相关对象,甚至是反向对象。

如果我从我的模型Pessoa 打印._meta.get_fields(),我会得到这些关系字段(我省略了“正常”字段):

<ManyToManyRel: cadastroimoveis.pessoa>
<ManyToOneRel: cadastroimoveis.pessoa_pessoa>
<ManyToOneRel: cadastroimoveis.pessoa_pessoa>
<ManyToOneRel: cadastroimoveis.pessoa_itr>
<ManyToManyRel: cadastroimoveis.doc>
<ManyToOneRel: cadastroimoveis.doc_pessoa>
cadastroimoveis.Pessoa.relacoes
cadastroimoveis.Pessoa.itrs

此特定模型只有 M2M 关系,并且都包含指定 Here 的“直通”模型。

如您所见,它重复了它们,一个用于模型,一个用于“通过”中间表(我猜也是一个模型)。而在递归关系的情况下,它会重复两次。

我的问题是,有没有办法让这些不重复?

一种知道哪些重复字段最终“指向”相同关系的方法(即使它向两个表发送垃圾邮件)?因为如果直通表有字段,我想以不同的方式显示它们。

根据Model _meta API 文档,您可以使用它来获取所有相关对象:

[
    f for f in MyModel._meta.get_fields()
    if (f.one_to_many or f.one_to_one)
    and f.auto_created and not f.concrete
]

但是“通过”表不被认为是自动创建的并且是具体的。

示例:

<ManyToManyRel: cadastroimoveis.ccir>
<ManyToOneRel: cadastroimoveis.ccir_pessoa>

这两个字段'指向'相同的关系,一个是中间表,另一个是模型,有没有(自动)方法可以知道这两个是相关的?我找不到他们共享的任何属性。

这是因为当直通表有字段时,我需要对其进行编辑,而不是模型本身的 M2M 字段

Models.py : http://pastebin.com/szDfhHQ3 我尽我所能地清理了

【问题讨论】:

你也可以在这里添加模型吗? 你使用的是哪个 Django 版本? @AKS 抱歉耽搁了,添加了我的模型,将很快测试当前答案 【参考方案1】:

例如,我们有这组模型。我是从this django example 收到的。

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
        Person,
        through='Membership',
        through_fields=('group', 'person'),
    )

class Membership(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    inviter = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        related_name="membership_invites",
    )
    invite_reason = models.CharField(max_length=64)

该解决方案看起来有点难看,但可以根据您的需要进行优化。

def get_through_field(f):                                                                                              
    opts = f.through._meta                                                                                             
    if f.through_fields:
        return opts.get_field(f.through_fields[1])                                                                     
    for field in opts.fields:                                                                                          
        rel = getattr(field, 'remote_field', None)                                                                     
        if rel and rel.model == f.model:                                                                               
            return field

model = models.Person

rels = dict(
    (f.field, f) for f in model._meta.get_fields()
    if f.is_relation
)

excludes = set()
for f in model._meta.get_fields():
    if f.many_to_many:
        through = get_through_field(f)
        excludes.add(rels[through])

for f in model._meta.get_fields():
    if f not in excludes:
        print f.name, f

输出

group <ManyToManyRel: m.group>
membership_invites <ManyToOneRel: m.membership>
id m.Person.id
name m.Person.name

如您所见,没有membership 字段。

【讨论】:

这是在哪个版本上测试的?我在 1.10 上尝试过,它在第 13 行给出了AttributeError: 'ManyToManyField' object has no attribute 'field' 在 Django==1.10.5 上工作正常。刚刚修复了代码中的错误,但它与您的问题无关。 ManyToManyField 看起来很奇怪,因为 get_fields() 返回的不是 Fields,而是 Rels。即使在您的示例中:&lt;ManyToManyRel: cadastroimoveis.ccir&gt;, &lt;ManyToOneRel: cadastroimoveis.ccir_pessoa&gt;。看?不应该有任何字段。请仔细检查。 这是因为我的模型也有一个“前向”关系作为一个字段,只需要过滤它们 我的代码看起来很糟糕。但这是我在 django.db.models.fields 代码的土地上发现的最好的东西。有很多丑陋的解决方案。 你的代码帮我弄清楚了我需要做什么,很快就会更新答案【参考方案2】:

对于 Django 1.10,以下代码的灵感来自 BaseModelForm 代码(Django 原创)。

如果你有以下关系:

class Group(Model):
    field = ....

class Person(Model):
    groups = ManyToManyField(Group, through='Membership')

class Membership(Model):
    person = ForeignKey(Person)
    group = ForeignKey(Group)
    position = TextField(...)

然后可以这样查询相关的字段和属性:

opts = Person._meta
for f in chain(opts.many_to_many, opts.virtual_fields):
    if f.rel.through:
        # this would return "group"
        attr_field = f.m2m_reverse_field_name()

        # this is the Membership class (a class instance)
        m2m_model = f.rel.through

        # this would return "person"
        join_field = field.m2m_field_name()

        # to get all "Membership" objects for "person" personXY
        qs_filter = join_field: personXY
        qs = m2m_model.objects.filter(**qs_filter)

        # get the PKs of all groups where personXY is a member of
        lookup_by_pk = '__pk'.format(attr_field)
        current_pks = qs.values_list(lookup_by_pk, flat=True) if qs.exists() else []

【讨论】:

这个例子没有得到反向关系 引用你的话:“这是因为当直通表有字段时,我需要对其进行编辑,而不是模型本身的 M2M 字段” - 如果您可以使用该代码执行此操作您有 Person 实例并希望保存存储在 Membership 模型中的其他数据。【参考方案3】:

其他答案肯定帮助我弄清楚了这一点,特别是在我的情况下,我所有的关系都是 M2M 并且有一个直通表,而且一切都是在 AJAX/javascript 中完成的,所以我的答案非常 JSON-y。

目前它只能通过 m2m 模型的表获取所有内容,因为您必须在其中创建对象才能创建关系,但它可以轻松扩展为获取所有其他关系

def get_relationships(model):
    fields = list(model._meta.get_fields())

    m2m_fields = 
    #Getting m2m relationships first
    for i, field in enumerate(fields):
        print(field)
        if field.is_relation:
            if field.many_to_many:
                fields.pop(i)
                try:
                    #If its a forward field, we want the relationship instead
                    if not hasattr(field,'field'):
                        field = field.remote_field
                except AttributeError:
                    pass
                if hasattr(field,'through'):
                    through = field.through
                    #In case of recursive relationships, there will be duplicates so we don't need to do it again
                    if m2m_fields.get(through._meta.model.__name__):
                        continue
                    m2m_fields[through._meta.model.__name__] = 
                    m2m = m2m_fields[through._meta.model.__name__]
                    #Finding the models which participate in the through table and the direction
                    m2m['owner'] = 'model' : field.model.__name__
                    m2m['related'] = 'model' : field.related_model.__name__
                    recursive = False
                    #Checking recursivity, will use this later
                    #Finding field names for the foreignkeys of the through table
                    for through_field in through._meta.get_fields():
                        if not (through_field.related_model is None):
                            if m2m['owner']['model'] == through_field.related_model.__name__ and not m2m['owner'].get('field'):
                                m2m['owner']['field'] = through_field.name
                            elif m2m['related']['model'] == through_field.related_model.__name__ and not m2m['related'].get('field'):
                                m2m['related']['field'] = through_field.name
                        elif not through_field.primary_key:
                            if not m2m.get('rel_fields'):
                                m2m['rel_fields'] = []
                            m2m['rel_fields'].append(through_field.name)
    #Now removing the through tables from the fields list, because they appear as a regular ManyToOne relationship otherwise
    for through_table in m2m_fields.keys():
        name = through_table
        for i, field in enumerate(fields):
            if field.many_to_one:
                if field.__name__ and field.related_model:
                    if field.related_model.__name__ == name:
                        fields.pop(i)
    #Todo : OneToOne and ManyToOne relationships

    return m2m_fields


for key,value in get_relationships(Pessoa).items():
    print(key, " = ", value)

这是一个非常丑陋的代码,但我不太擅长 Python,只是想学习一些东西,但我保证它对我的问题很有吸引力

【讨论】:

以上是关于Django使用“通过”关系表从模型中获取所有相关对象的主要内容,如果未能解决你的问题,请参考以下文章

使用关系表从数据库中获取记录时出现问题

Django - 按模型名称获取 ContentType 模型(通用关系)

使用反向关系在 django 模型中获取 n 个项目

Django:在多对多关系中查找所有值,其中相关集合的所有元素都符合特定条件

获取所有相关的 Django 模型对象

Django get all,带有相关模型