I have two simple models, one representing a movie an the other representing a rating for a movie.
class Movie(models.Model):
id = models.AutoField(primary_key=True)
title = models.TextField()
class Rating(models.Model):
id = models.AutoField(primary_key=True)
movie = models.ForeignKey(Movie)
rating = models.FloatField()
My expectation is that I would be able to first create a Movie and a Review referencing that movie then commit them both to the database, as long as I committed the Movie first so that it was given a primary key for the Review to refer to.
the_hobbit = Movie(title="The Hobbit")
my_rating = Rating(movie=the_hobbit, rating=8.5)
the_hobbit.save()
my_rating.save()
To my surprise it still raised an IntegrityError complaining that I was trying to specify a null foreign key, even the Movie had been committed and now had a primary key.
IntegrityError: null value in column "movie_id" violates not-null constraint
I confirmed this by adding some print statements:
print "the_hobbit.id =", the_hobbit.id # None
print "my_rating.movie.id =", my_rating.movie.id # None
print "my_rating.movie_id =", my_rating.movie_id # None
the_hobbit.save()
print "the_hobbit.id =", the_hobbit.id # 3
print "my_rating.movie.id =", my_rating.movie.id # 3
print "my_rating.movie_id =", my_rating.movie_id # None
my_rating.save() # raises IntegrityError
The .movie attribute is referring to a Movie instance which does have a non-None .id, but .movie_id is holding into the value None that it had when the Movie instance was crated.
I expected Django to look up .movie.id when I tried to commit the Review, but apparently that’s not what it’s doing.
Aside
In my case, I’ve dealt this this behaviour by overriding the .save() method on some models so that they look up the primary keys of foreign keys again before saving.
def save(self, *a, **kw):
for field in self._meta.fields:
if isinstance(field, ForeignKey):
id_attname = field.attname
instance_attname = id_attname.rpartition("_id")[0]
instance = getattr(self, instance_attname)
instance_id = instance.pk
setattr(self, id_attname, instance_id)
return Model.save(self, *a, **kw)
This is hacky, but it works for me so I am not really looking for a solution to this particular problem.
I am looking for an explanation of Django’s behaviour. At what points does Django look up the primary key for foreign keys? Please be specific; references to the Django source code would be best.
Looking in the Django source, the answer lies in some of the magic Django uses to provide its nice API.
When you instantiate a
Ratingobject, Django sets (though with some more indirection to make this generic)self.movietothe_hobbit. However,self.movieisn’t a regular property, but is rather set through__set__. The__set__method (linked above) looks at the value (the_hobbit) and tries to set the propertymovie_idinstead ofmovie, since it’s aForeignKeyfield. However, sincethe_hobbit.pkis None, it just setsmovietothe_hobbitinstead. Once you try to save your rating, it tries to look upmovie_idagain, which fails (it doesn’t even try to look atmovie.)Interestingly, it seems this behaviour is changing in Django 1.5.
Instead of
it now does
which in your case would result in a more helpful error message.