Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When does Django look up the primary key of foreign keys?

Tags:

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.

like image 256
Jeremy Avatar asked Nov 29 '12 17:11

Jeremy


People also ask

How does Django define primary key and foreign key?

What's the difference between foreign key and primary key? The primary key defines the unique value for a row in a table. Foreign key connects tables using the primary key value from another table.

How does foreign key work in Django?

Introduction to Django Foreign Key. A foreign key is a process through which the fields of one table can be used in another table flexibly. So, two different tables can be easily linked by means of the foreign key. This linking of the two tables can be easily achieved by means of foreign key processes.

How do I refer primary key in Django?

If you'd like to specify a custom primary key, specify primary_key=True on one of your fields. If Django sees you've explicitly set Field.primary_key , it won't add the automatic id column. Each model requires exactly one field to have primary_key=True (either explicitly declared or automatically added).

How can you tell primary key from foreign key?

A primary key is a special key in a relational database that acts as a unique identifier for each record meaning it uniquely identifies each row/record in a table and its value should be unique for each row of the table. A foreign key, on the other hand, is a field in one table that link two tables together.


2 Answers

As stated by the docs:

The keyword arguments are simply the names of the fields you’ve defined on your model. Note that instantiating a model in no way touches your database; for that, you need to save().

Add a classmethod on the model class:

class Book(models.Model):     title = models.CharField(max_length=100)      @classmethod     def create(cls, title):         book = cls(title=title)         # do something with the book         return book  book = Book.create("Pride and Prejudice") 

Add a method on a custom manager (usually preferred):

class BookManager(models.Manager):     def create_book(self, title):         book = self.create(title=title)         # do something with the book         return book  class Book(models.Model):     title = models.CharField(max_length=100)      objects = BookManager()  book = Book.objects.create_book("Pride and Prejudice") 

origin: https://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#creating-objects

When you assign the_hobbit, you are assigning an instance of Movie, thus not hitting the database. Once you call 'save' the database does fill up, however your variable is still pointing to the object in memory, not aware of the sudden database change.

That said, changing the order of your sequence should also effectively create the objects:

the_hobbit = Movie(title="The Hobbit") the_hobbit.save() my_rating = Rating(movie=the_hobbit, rating=8.5) my_rating.save() 
like image 97
Hedde van der Heide Avatar answered Dec 03 '22 08:12

Hedde van der Heide


The main issue has to do with side effects that are wanted or not. And with variables really being pointers to objects in Python.

When you create an object out of a model, it doesn't have a primary key yet as you haven't saved it yet. But, when saving it, should Django have to make sure it updates attributes on the already-existing object? A primary key is logical, but it would also lead you to expect other attributes being updated.

An example for that is Django's unicode handling. Whatever charset you give the text you put into a database: Django gives you unicode once you get it out again. But if you create an object (with some non-unicode attribute) and save it, should Django modify that text attribute on your existing object? That already sounds a little bit more dangerous. Which is (probably) why Django doesn't do any on-the-fly updating of objects you ask it to store in the database.

Re-loading the object from database gives you a perfect object with everything set, but it also makes your variable point to a different object. So that would not help in your example in case you already gave the Rating a pointer at your "old" Movie object.

The Movie.objects.create(title="The Hobbit") mentioned by Hedde is the trick here. It returns a movie object from the database, so it already has an id.

the_hobbit = Movie.objects.create(title="The Hobbit") my_rating = Rating(movie=the_hobbit, rating=8.5) # No need to save the_hobbit, btw, it is already saved. my_rating.save() 

(I had problems with the difference between my objects and objects from the database, too, when my newly created object didn't output unicode. The explanation I put on my weblog is the same as above, but worded a bit differently.)

like image 34
Reinout van Rees Avatar answered Dec 03 '22 07:12

Reinout van Rees