djangoforeign-keysdjango-ormgeneric-foreign-key

Generic Foreign Key on unsaved model in Django


I have stumbled into a bit of inconsistency with Foreign Keys and Generic Foreign Keys in Django. Suppose we have three models

class Model_One(models.Model):
    name= models.CharField(max_length=255)

class Model_with_FK(models.Model):
    name=models.CharField(max_length=255)
    one=models.ForeignKey(Model_One, on_delete=models.CASCADE)

class Model_With_GFK(models.Model):
    name=models.CharField(max_length=255)
    content_type= models.ForeignKey(
        ContentType, on_delete=models.CASCADE,
    )
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")
    class Meta:
        indexes=[models.Index(fields=["content_type", "object_id"]), ]

When we do

one = Model_One()
two = Model_Two(model_one=one)
one.save()
two.save()

Everything works okay. Model one recieves an ID, and when model two is saved, that ID is used to reference model one. However, the same does not work with GFK

one = Model_One()
two = Model_With_GFK(content_object=one)
one.save()
two.save()

When there is an attempt to save two, integrity error is raised that "object_id" column is null. When inspected with debugger, as soon as model one is saved, field "content_object"on the model two is turn from model "one" to None. This is quite unexpected, as with Foreign Key there is no such problem.

Of course you can save model one before using it in GFK relation, but why is this necessary? With Foreign keys, i could instantiate all the models and then create them with bulk_create. With GFK in the picture, this no longer seems possible


Solution

  • Django's GenericForeignKey has been implemented in a bit more "boring" way. Indeed, it works with the .set(…) descriptor [GitHub]:

    def __set__(self, instance, value):
        ct = None
        fk = None
        if value is not None:
            ct = self.get_content_type(obj=value)
            fk = value.pk
    
        setattr(instance, self.ct_field,ct)
        setattr(instance, self.fk_field, fk)
        self.set_cached_value(instance, value)

    It thus does not do that much, besides finding the content_type and the primary key for the value you select, and sets the ct_field (content type), and the fk_field (the field that stores primary keys).

    Strictly speaking, it could be done by rewriting the the ._prepare_related_fields_for_save(…) [GitHub] to postpone the saving until you save the object to the database, but this thus has not been done.

    N.B.: I opened a ticket [Django-ticket] for this.