Django: Cannot create model with a Field that maps to different models

Viewed 224

In my app, I have a model called Supplier and multiple models like Part, Fuel, Ink, etc., which are all a some form of resource that a supplier might supply.

I want to be able to fetch all resources for a given supplier, and figured I would implement this efficiently by making a SupplierResource model with two fields: supplier and resource (resource be a foreign key to an object in either one of the "resource" tables).

But this, does not seem to work, because I can't create a foreign key that maps to different models.

Here are example models:

class Supplier(models.Model):
    delivery_time = models.CharField(max_length=255, blank=True, null=True)
    minimum_order_quantity = models.FloatField(blank=True, null=True)
    billing_address = models.ForeignKey(...)
    shipping_address = models.ForeignKey(...)
    # ...


class Resource(models.Model):
    code = models.CharField(max_length=255, unique=True, blank=True, null=True)
    supplier = models.ForeignKey(
        Company,
        related_name="supplier_part",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    country = models.CharField(max_length=255, blank=True, null=True)

    class Meta:
        abstract = True

class Part(Resource):
    some_attr = models.CharField(max_length=255, blank=True, null=True)


class Fuel(Resource):
    some_other_attr = models.CharField(max_length=255, blank=True, null=True)

I could iterate through all child models of Resource to collect resources for a given supplier but that would be very slow.

I would prefer not using polymorphism to avoid unnecessary complexity (unless it turns out this is strictly necessary).

3 Answers

You could consider to use django-polymorphic.

If you do this, just let Resource inherit from PolymorphicModel like this:

from polymorphic.models import PolymorphicModel

class Supplier(models.Model):
    delivery_time = models.CharField(max_length=255, blank=True, null=True)
    minimum_order_quantity = models.FloatField(blank=True, null=True)


class Resource(PolymorphicModel):
    code = models.CharField(max_length=255, unique=True, blank=True, null=True)
    country = models.CharField(max_length=255, blank=True, null=True)


class Part(Resource):
    some_attr = models.CharField(max_length=255, blank=True, null=True)


class Fuel(Resource):
    some_other_attr = models.CharField(max_length=255, blank=True, null=True)

And then you can create the SupplierResource model like the following:

class SupplierResource(models.Model):
    supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
    resource = models.ForeignKey(Resource, on_delete=models.CASCADE)

Or, a many to many relations between Supplier and Resource:

class Supplier(models.Model):
    delivery_time = models.CharField(max_length=255, blank=True, null=True)
    minimum_order_quantity = models.FloatField(blank=True, null=True)
    resources = models.ManyToManyField(Resource, related_name="suppliers")

As mentioned by C14L you can use a GenericForeignKey to create a relationship with another model. However GenericForeignKeys come with their own issues. They don't create reverse relationships. So you wouldn't be able to do, for example, [sr.supplier for sr in Part.supplier_resources], however it would allow you to get rid of your Resource parent model. What you've got with the Resource parent model is the right idea I believe. I would suggest in the resource model, you add a field a field called child_model_name

class Resource(models.Model):
    child_model_name = models.TextField()
    some_attr = models.CharField(max_length=255, blank=True, null=True)

where child_model_name = 'part' etc..

Then in your SupplierResource Model you have

class SupplierResource(models.Model):
    supplier = models.ForeignKey(Supplier, ..., related_name="supplier_resources")
    resource = models.ForeignKey(Resource, ..., related_name="supplier_resources")

Here what django is doing for you is creating a table called Resource that contains all the fields for a Resource. It also creates a table for you called Part with fields specific to a part that has a OneToOne relationship to Resource.

Now can you do this:

parts = Part.objects.filter(supplier_rescources__supplier=supplier)

Or to get different types of resource in one list (Part, fuel etc).

resources = Resource.objects.filter(supplier_resources__supplier=supplier)

[getattr(resource, resource.child_model_name) for resource in resources]
Related