【问题标题】:Synonymous many to many model relationship in DjangoDjango中的同义多对多模型关系
【发布时间】:2019-11-29 11:20:57
【问题描述】:

我正在尝试在 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])

现在当我得到兔子的同义词时,它有我想要的:

(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 做到这一点,但我有疑问。

我最好只写一个手动管理这些关系的信号吗?

【问题讨论】:

    标签: django django-models django-orm


    【解决方案1】:

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

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

    • 1234563适用于行/实例集合的正向模型管理器(例如 models.Managerobjects 访问)

    为了得到你想要的,你可以创建一个辅助函数来创建传递的对象之间的所有相互关系:

    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】:

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

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

      编辑

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

      【讨论】:

        【解决方案3】:

        作为heemayl mentioned,直接通过ORM是没有办法的。

        所以我最终制作了 2 个函数,一个用于获取/查找所有同义关系,另一个用于设置/同步它们

        获取/查找:

        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)
        

        【讨论】:

          猜你喜欢
          • 2015-02-03
          • 2012-12-04
          • 2018-01-04
          • 2019-05-09
          • 2012-07-28
          • 1970-01-01
          • 2012-07-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多