Reputation: 2398
I want to create a foreign key that can have a reference to exactly one of three other models. example:
user
fav_animal: foreign_key(bear, cat, dog)
bear
name
cat
name
dog
name
How can I achieve this in django? Do I just create 3 nullable foreign keys? Then how do I enforce that always exactly one of them is set?
Upvotes: 1
Views: 187
Reputation: 476659
There are some options, but regardless what option you use, such design does not really fit relational databases. Relational databases are conceptually different from object-oriented modeling, and although one can put a lot of effort into mapping between the two worlds (what an ORM is basically doing), it will still never work perfectly.
You can create a model Animal
that you then subclass to Cat
, Dog
, etc.:
class Animal(models.Model):
name = models.CharField(max_length=128)
class Bear(models.Model):
pass
class Cat(models.Model):
pass
class Dog(models.Model):
pass
class User(models.Model):
fav_animal = models.ForeignKey(Animal, on_delete=models.PROTECT)
Here Django will create a table for Animal
and tables for Bear
, Cat
and Dog
. The Animal
, Bear
and Cat
will have an animal_ptr
column that refers to the Animal
. So that means that an Animal
with id=14
will store the name for a Bear
, Cat
or Dog
(or some other subclass) with id=14
.
The advantage of this approach is that you can still query on the columns of the animal. For example, you can query on users that have a fav_animal
that has as name 'Diesel'
with:
User.objects.filter(fav_animal__name='Diesel')
But when we retrieve that animal, it is just an Animal
, not a dog anymore:
>>> User.objects.get(fav_animal__name='Diesel').fav_animal
<Animal: Animal object (1)>
We can however obtain the Dog
of that animal by accessing the .dog
attribute:
>>> User.objects.get(fav_animal__name='Diesel', fav_animal__dog__isnull=False).fav_animal.dog
<Dog: Dog object (1)>
We can furthermore filter, by specifying fav_animal__dog__isnull=False
, or filter all User
s that have not a dog as favorite animal with:
# all users that have not a dog as animal
User.objects.filter(fav_animal__dog=None)
If a dog
for example has some extra attribute, we can filter on that as well:
# all users that have a dog as animal who's food is a bone
User.objects.filter(fav_animal__dog__food='bone')
The main disadvantage is that it will make obtaining data less convenient, since Django now has to make JOINs to obtain the data in the related Animal
object.
Django's content type framework keeps track of the types, and has an extra table of types. We can use that and store the primary key, and the type of an object, and those to combined refer to an item. This however has some problems: the database can not easily enforce that this GenericForeignKey
[Django-doc] refers to a valid object. Indeed, since the reference is only valid if the primary key has a value in the table to which the GenericForeignKey
is pointing. Another disadvantage is that querying on related fields is harder. You can query with the object itself (Django will translate that in a query on both the type field, and the primary key), but more sophisticated queries are harder.
We then can thus generate such model with:
from django.db.models import Q
class Bear(models.Model):
pass
class Cat(models.Model):
pass
class Dog(models.Model):
pass
class User(models.Model):
object_id = models.PositiveIntegerField()
content_type = models.ForeignKey(
ContentType,
limit_choices_to=Q(app_label='app', model='bear') |
Q(app_label='app', model='cat') |
Q(app_label='app', model='dog')
)
content_object = GenericForeignKey('content_type', 'object_id')
But as said advanced querying is not possible.
Upvotes: 1
Reputation: 1904
You might want to use generic foreign key, as described here: https://docs.djangoproject.com/en/2.2/ref/contrib/contenttypes/#generic-relations.
For your example, code might look like this:
# models.py
class User(models.Model):
name = models.CharField(max_length=100)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
animal_id = models.PositiveIntegerField(null=True)
animal = GenericForeignKey('content_type', 'animal_id')
class Bear(models.Model):
name = models.CharField(max_length=100)
class Cat(models.Model):
name = models.CharField(max_length=100)
class Dog(models.Model):
name = models.CharField(max_length=100)
# And the usage:
In [1]: b1=Bear.objects.create(name='b1')
In [2]: u1=User.objects.create(name='u1', animal=b1)
In [3]: u1.animal
Out[3]: <Bear: Bear object (1)>
In [4]: u1.animal_id
Out[4]: 1
Upvotes: 1