Understanding Django GenericForeignKey and GenericRelation
Solution 1:
GenericForeignKey try to give you a ForeignKey
behavior but instead to be against one type of object, they do it for a set of object types (thats why they are defined with 2 columns, 1 to keep the primary_key
and another to keep the contenty_type
).
GenericRelation
is the reverse relation of a GenericForeignKey
, because Django do not automatically create reverse relations for GenericForeignKeys
(unlike ForeignKeys
) you have to setup them manually.
I'm not very familiar with the best-practices in translations/vocabulary staffs, but if you want to approach the problem with GenericRelations
and GenericForeignKeys
, one way to do it would be:
class Word(Model):
name = CharField(max_length=75)
nouns = GenericRelation('WordNoun', content_type_field='noun_ct', object_id_field='noun_id')
class WordNoun(Model):
word = ForeignKey(Word)
noun_ct = ForeignKey(ContentType,
on_delete=models.CASCADE,
#this is optional
limit_choices_to = {"model__in": ('EnNoun', 'FrNoun')}
)
noun_id = PositiveIntegerField()
noun = GenericForeignKey('noun_ct', 'noun_id')
class EnNoun(Model):
word = OneToOneField(Word)
class FrNoun(Model):
word = ForeignKey(Word)
gender = CharField()
We are basically creating a model keeping word-noun relations, this give is the following
# Having some word
word = Word.objects.get(pk=1)
# With 1 query you can get a list with
# all WordNoun objects for this word.
word_nouns = word.nouns.all()
The problem with this approach is that after you get the word_nouns
list,
accessing a single noun
instance will make a new query.
for word_noun in word.nouns.all():
print word_noun.noun #this will make a query
One way to slightly optimize this is to use prefetch_related
, so if a single word
has 3 word_nouns
(lets say 1 EnNoun
and 2 FrNoun
).
Then instead of 4 queries - 1 for the word_nouns
and 3 for each noun
, we optimize it to 3 queries - 1 for the word_nouns
and 2 for each contenty_type
(EnNoun
and FrNoun
)
for word_noun in word.nouns.all().prefetch_related('noun'):
print word_noun.noun #this will not make a query
The difference is that the number of queries will now depends on the number of different ContentTypes
, rather than the number of related WordNoun
objects.
This scales nice when you have several Nouns
from one contenty_type
in the list you are prefetching, but will make no difference if you have 1 Noun per
contenty_type`.
Another approach which I can think of is to use the following model structure:
class Word(Model):
name = CharField(max_length=75)
class WordNoun(Model):
LANG_CHOICES = (
('en', 'English'),
('fr', 'French'),
)
word = ForeignKey(Word)
lang = models.CharField(max_length=2, choices=LANG_CHOICES)
gender = CharField(max_length=2, blank=True, default='')
#Now accessing all word_nouns would as easy as:
word_nouns = word.wordnoun_set.all()
Solution 2:
It is possible to add several GenericForeignKey
to the same model. This would allow you to create a "link" between objects with generic type. I give an example latter. I modified your example to create something more useful in my sense. The table Translation
create links between French and English words. Words are stored in classes EnVerb
, FrVerb
, EnNoun
and FrNoun
. Without GenericForeignKey
, you would have to create two translation models: TranslationVerb
and TranslationNoun
. But in the following code, I show a generic translation model working for both verbs, and nouns. I hope it makes sense!
More technically, you forgot to add the GenericRelation
field. Also, they must specify the new names of the fields content_type
and object_id
in the GenericRelation
field of the related objects.
EXAMPLE
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
Translation(models.Model):
fr_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name="fr_content_type")
fr_object_id = models.PositiveIntegerField()
fr_word = GenericForeignKey('fr_content_type', 'fr_object_id')
en_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name="en_content_type")
en_object_id = models.PositiveIntegerField()
en_word = GenericForeignKey('en_content_type', 'en_object_id')
class FrVerb(models.Model):
name = models.CharField(max_length=75)
translation = GenericRelation(Translation, content_type_field='fr_content_type', object_id_field='fr_object_id')
class FrNoun(models.Model):
name = models.CharField(max_length=75)
gender = models.CharField(max_length=75)
translation = GenericRelation(Translation, content_type_field='fr_content_type', object_id_field='fr_object_id')
class EnVerb(models.Model):
name = models.CharField(max_length=75)
translation = GenericRelation(Translation, content_type_field='en_content_type', object_id_field='en_object_id')
class EnNoun(models.Model):
name = models.CharField(max_length=75)
translation = GenericRelation(Translation, content_type_field='en_content_type', object_id_field='en_object_id')
You can use this to create generic translations between models :
from polls.models import *
EnNoun(name='tree').save()
FrNoun(name='arbre').save()
EnVerb(name='be').save()
FrVerb(name='etre').save()
trans1 = Translation(fr_word=FrNoun.objects.first(), en_word=EnNoun.objects.first())
trans2 = Translation(fr_word=FrVerb.objects.first(), en_word=EnVerb.objects.first())
Now trans1
is used to link 'tree' and 'arbre' and trans2
to link 'be' et 'être' while these objects belongs to different models !