Django中多对多模型关系的同义词

时间:2019-11-29 11:20:57

标签: django django-models django-orm

我正在尝试在Django的多对多字段自引用上实现所谓的“同义”关系。

以该模型为例(实际上,我不使用真实词,而是使用类别标签):

class Word(models.Model):
    name = models.CharField(max_length=30, unique=True)
    synonymous = models.ManyToManyField('self', blank=True,  related_name='synonymous')

    def __str__(self):
        return self.name

我要实现的是,当我有3个对象并将它们的任何组合添加到同义词字段中时,我希望所有这些对象都可以连接。

# Create synonymous words
bunny = Word.objects.create(name='bunny')
hare = Word.objects.create(name='hare')
rabbit = Word.objects.create(name='rabbit')
# Set synonymous words to rabbit
rabbit.synonymous.set([bunny, hare])

现在,当我获得Rabbit的同义词对象时,它具有我想要的:

(Pdb) rabbit.synonymous.all()
<QuerySet [<Word: bunny>, <Word: hare>]>

但是当我把兔子和野兔的同义词当作对象时,它们只会返回兔子。

(Pdb) bunny.synonymous.all()
<QuerySet [<Word: rabbit>]>
(Pdb) hare.synonymous.all()
<QuerySet [<Word: rabbit>]>

我想要实现的是所有同义对象,都是“对称的”。现在,m2m字段已经是对称的,但是它仅停留在一个对象上,而不是所有给定的同义对象。

所以,理想的结果是这样:

# Create synonymous words
bunny = Word.objects.create(name='bunny')
hare = Word.objects.create(name='hare')
rabbit = Word.objects.create(name='rabbit')
# Set synonymous ON ONE WORD
rabbit.synonymous.set([bunny, hare])

# And now ALL the objects, which have at least ONE related synonym, would automatically be assigned to the other words as well
(Pdb) rabbit.synonymous.all()
<QuerySet [<Word: bunny>, <Word: hare>]>
(Pdb) hare.synonymous.all()
<QuerySet [<Word: bunny>, <Word: rabbit>]>
(Pdb) bunny.synonymous.all()
<QuerySet [<Word: rabbit>, <Word: hare>]>

我希望这很清楚。

我想知道最干净的方法是什么?也许有一些方法可以通过ORM来做到这一点,但我对此表示怀疑。

我最好只是写一个信号来手动管理那些关系吗?

3 个答案:

答案 0 :(得分:1)

我认为最简单的方法是通过以下代码:

for word in synonymous:
    word.synonymous.set(synonymous.exclude(pk=word.pk))

修改

放置此代码的最佳位置是您的views.py之内。如果您使用的是Django Admin,则应使用save_related

答案 1 :(得分:1)

无法像这样一次创建所有对称关系的直接方法,因为:

  • rabbit.synonymous是一个描述符(ManyToManyDescriptor),它实际上返回有关实例属性访问的ManyRelatedManager

  • 由于多对多管理器将关系附加到单个对象,因此ManyRelatedManager没有任何预期的方法,因为它是单个实例的属性(rabbit这种情况)不同于适用于行/实例集合

  • 的前向模型管理器(例如,以models.Manager访问的objects

要获得所需的内容,可以创建一个辅助函数来创建传递的对象之间的所有相互关系:

from itertools import combinations

def create_m2m_inter_relations(*objs): 
    if len(objs) < 2: 
        raise ValueError(
            'There must be at least two objects '
            'passed to create relationship.'
        ) 

    if len(objs) == 2: 
        objs[0].set([objs[1]]) 
        return 

    objs = set(objs) 
    relations_map = {
        next(iter(objs.difference(comb))): comb
        for comb in combinations(objs, len(objs) - 1)
    }  
    for instance, relations in relations_map.items():
        instance.synonymous.set(relations)

注意事项:

  • 由于Django M2M关系是对称的,因此对于在循环的早期迭代中已建立的关系,以上内容重复了相同的set操作。读者可以自己删除重复的set操作。

  • 当您与所有对象创建关系时,ManyToManyDescriptor-synonymous的名称相同,因此该函数在函数中进行了硬编码,在这种情况下可以正常工作。但是,如果您想要更通用的解决方案,请考虑到这一点并进行相应的修改。

答案 2 :(得分:0)

heemayl mentioned一样,无法直接通过ORM进行操作。

所以我最终完成了两个功能,一个用于获取/查找所有同义词关系,另一个用于设置/同步它们

获取/查找:

def get_all_synonymous_words(word):
    # Start with the word's own synonymous words, and their synonymous words
    found_syn_ids = set(word.synonymous.values_list('id', flat=True)) | set(word.synonymous.values_list('synonymous', flat=True))

    while found_syn_ids:
        words = Word.objects.filter(id__in=found_syn_ids)
        batch_syn_ids = set(words.values_list('synonymous', flat=True))

        # If the batch syn ids are equal to last batch ids, all relationships found
        if batch_syn_ids == found_syn_ids:
            return batch_syn_ids

        found_syn_ids = batch_syn_ids

用于设置/同步:

def set_all_synonymous_words(word):
    syn_ids = get_all_synonymous_words(word)
    if syn_ids:
        syn_qs = Word.objects.filter(id__in=syn_ids)
        for word in syn_qs:
            # Exclude current word to not self reference
            excluded_qs = syn_qs.exclude(id=word.id)
            if not set(word.synonymous.exclude(id=word.id)) == excluded_qs:
                word.synonymous.set(excluded_qs)

这样,当您更新/创建单词时,您可以找到所有组合并进行更新,而无需预先定义同义词列表。

当我使用django-rest-framework时,我通过覆盖perform_update / perform_create方法在视图中将其调用:

    def perform_update(self, serializer):
        serializer.save()

        # Get the updated word, and sync the synonymous words to be all symmetrical
        set_all_synonymous_words(Word.objects.get(id=serializer.data['id']))


    def perform_create(self, serializer):
        serializer.save()

        # Get the updated word, and sync the synonymous words to be all symmetrical
        set_all_synonymous_words(Word.objects.get(id=serializer.data['id']))

或者您可以手动将其调出:

    # Create synonymous words
    bunny = Word.objects.create(name='bunny')
    hare = Word.objects.create(name='hare')
    rabbit = Word.objects.create(name='rabbit')
    # Set synonymous
    rabbit.synonymous.set([bunny, hare])
    # Sync synonymous
    set_all_synonymous_words(bunny)