user2741831
user2741831

Reputation: 2398

How can I create a model in django that has a foreign key to one of multiple models?

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

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

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.

Option 1: Inheritance

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 Users 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.

Option 2: Generic foreign keys

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

Eugene Prikazchikov
Eugene Prikazchikov

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

Related Questions