2010-11-09 3 views
3
class Food_Tag(models.Model): 
    name = models.CharField(max_length=200) 
    related_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation') 

    def __unicode__(self): 
    return self.name 

class Tag_Relation(models.Model): 
    source = models.ForeignKey(Food_Tag, related_name='source_set') 
    target = models.ForeignKey(Food_Tag, related_name='target_set') 
    is_a = models.BooleanField(default=False); # True if source is a target 
    has_a = models.BooleanField(default=False); # True if source has a target 

Je veux être en mesure d'obtenir les relations entre Food_Tags comme:Comment créer des relations récursives ManyToManyField avec des champs supplémentaires symétriques dans Django?

>>> steak = Food_Tag.objects.create(name="steak") 
>>> meat = Food_Tag.objects.create(name="meat") 
>>> r = Tag_Relation(source=steak, target=meat, is_a=True) 
>>> r.save() 
>>> steak.related_tags.all() 
[<Food_Tag: meat>] 
>>> meat.related_tags.all() 
[] 

mais related_tags est vide pour la viande. Je me rends compte que cela a à voir avec l'argument 'symmetrical = False', mais comment puis-je configurer le modèle de telle sorte que 'meat.related_tags.all()' renvoie tous les Food_Tags associés?

Répondre

4

Comme mentionné dans the docs:

Ainsi, il est pas (encore?) Possible d'avoir un symétrique, récursif plusieurs à plusieurs avec des champs supplémentaires, dans Django. C'est un "deux choix" affaire.

1

Puisque vous n'avez pas explicitement dit qu'ils doivent être asymétriques, la première chose que je vais suggérer est de définir symmetrical=True. Cela entraînera la relation à travailler dans les deux sens comme vous l'avez décrit. Comme eternicode l'a fait remarquer, vous ne pouvez pas le faire lorsque vous utilisez un modèle through pour la relation M2M. Si vous pouvez vous permettre de vous passer du modèle through, vous pouvez définir symmetrical=True pour obtenir exactement le comportement que vous décrivez.

Si elles doivent rester asymétrique cependant, vous pouvez ajouter l'argument mot-clé related_name="sources" au champ related_tags (que vous pouvez envisager de renommer à targets pour rendre les choses plus claires), puis accéder aux balises connexes à l'aide meat.sources.all().

2

J'ai trouvé cette approche faite par Charles Leifer qui semble être une bonne approche pour surmonter cette limitation de Django.

0

Pour créer une relation symétrique, vous avez deux options:

1) Créez deux objets Tag_Relation - une avec steak comme source, et une autre avec steak comme cible:

>>> steak = Food_Tag.objects.create(name="steak") 
>>> meat = Food_Tag.objects.create(name="meat") 
>>> r1 = Tag_Relation(source=steak, target=meat, is_a=True) 
>>> r1.save() 
>>> r2 = Tag_Relation(source=meat, target=steak, has_a=True) 
>>> r2.save() 
>>> steak.related_tags.all() 
[<Food_Tag: meat>] 
>>> meat.related_tags.all() 
[<Food_Tag: steak] 

2) Ajouter un autre ManyToManyField au modèle Food_Tag:

class Food_Tag(models.Model): 
    name = models.CharField(max_length=200) 
    related_source_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation', through_fields=('source', 'target')) 
    related_target_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation', through_fields=('target', 'source')) 

class Tag_Relation(models.Model): 
    source = models.ForeignKey(Food_Tag, related_name='source_set') 
    target = models.ForeignKey(Food_Tag, related_name='target_set') 

Comme note, je vais essayer d'utiliser quelque chose plus descriptif que source et target pour les champs de modèle.

0

La meilleure solution de ce problème (après de nombreuses enquêtes) était de créer manuellement l'enregistrement db symétrique sur appel save(). Il en résulte une redondance des données DB, bien sûr, car vous créez deux enregistrements au lieu d'un.Dans votre exemple, après avoir sauvé Tag_Relation(source=source, target=target, ...) vous devez enregistrer relation inverse Tag_Relation(source=target, target=source, ...) comme ceci:

class Tag_Relation(models.Model): 
    source = models.ForeignKey(Food_Tag, related_name='source_set') 
    target = models.ForeignKey(Food_Tag, related_name='target_set') 
    is_a = models.BooleanField(default=False); 
    has_a = models.BooleanField(default=False); 

    class Meta: 
     unique_together = ('source', 'target') 

    def save(self, *args, **kwargs): 
     super().save(*args, **kwargs) 

     # create/update reverse relation using pure DB-level functions 
     # we cannot just save() reverse relation because there will be a recursion 
     reverse = Tag_Relation.objects.filter(source=self.target, target=self.source) 
     if reverse.exists(): 
      reverse.update(is_a=self.is_a, has_a=self.has_a) 
     else: 
      Tag_Relation.objects.bulk_create([ 
       Tag_Relation(source=self.target, target=self.source, is_a=self.is_a, has_a=self.has_a) 
      ]) 

Le seul inconvénient de cette mise en œuvre dédouble entrée Tag_Relation, mais sauf ce que tout fonctionne bien, vous pouvez même utiliser Tag_Relation dans InlineAdmin.

MISE À JOUR Ne pas oublier de définir la méthode delete et qui Détruit une relation inverse.

+0

En fait, Django implémente la symétrie de cette façon. Il crée un autre enregistrement dans la base de données. – Bobort

Questions connexes