Django什么时候查找外键的主键?

时间:2022-07-16 11:29:28

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抱怨我正在尝试指定一个空外键,即使Movie已经提交并且现在有一个主键。

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.

.movi​​e属性指的是一个具有非None .id的Movie实例,但.movi​​e_id保持在Movie实例被装箱时的值None。

I expected Django to look up .movie.id when I tried to commit the Review, but apparently that's not what it's doing.

当我试图提交评论时,我预计Django会查找.movi​​e.id,但显然这不是它正在做的事情。


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.

在我的例子中,我通过覆盖某些模型上的.save()方法来处理这种行为,以便在保存之前再次查找外键的主键。

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.

这很hacky,但它对我有用,所以我不是真的在寻找这个特定问题的解决方案。


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.

我正在寻找Django行为的解释。 Django在什么时候查找外键的主键?请具体说明;对Django源代码的引用是最好的。

5 个解决方案

#1


9  

Looking in the Django source, the answer lies in some of the magic Django uses to provide its nice API.

看看Django源代码,答案在于Django用来提供其优秀API的一些神奇功能。

When you instantiate a Rating object, Django sets (though with some more indirection to make this generic) self.movie to the_hobbit. However, self.movie isn'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 property movie_id instead of movie, since it's a ForeignKey field. However, since the_hobbit.pk is None, it just sets movie to the_hobbit instead. Once you try to save your rating, it tries to look up movie_id again, which fails (it doesn't even try to look at movie.)

当你实例化一个Rating对象时,Django设置(虽然有一些更多的间接使这个通用)self.movi​​e到the_hobbit。但是,self.movi​​e不是常规属性,而是通过__set__设置。 __set__方法(上面链接)查看值(the_hobbit)并尝试设置属性movie_id而不是movie,因为它是ForeignKey字段。但是,由于the_hobbit.pk为None,它只是将电影设置为the_hobbit。一旦你试图保存你的评级,它会再次尝试查看movie_id,这会失败(它甚至不会尝试查看电影。)

Interestingly, it seems this behaviour is changing in Django 1.5.

有趣的是,似乎这种行为在Django 1.5中正在发生变化。

Instead of

代替

setattr(value, self.related.field.attname, getattr(
    instance, self.related.field.rel.get_related_field().attname))
# "self.movie_id = movie.pk"

it now does

现在呢

    related_pk = getattr(instance, self.related.field.rel.get_related_field().attname)
    if related_pk is None:
        raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
                            (value, instance._meta.object_name))

which in your case would result in a more helpful error message.

在您的情况下,这将导致更有用的错误消息。

#2


10  

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:

在模型类上添加classmethod:

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

来源: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.

分配the_hobbit时,您将分配一个Movie实例,因此不会访问数据库。一旦调用'save',数据库就会填满,但是你的变量仍然指向内存中的对象,而不知道数据库的突然变化。

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()

#3


10  

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

主要问题与想要或不想要的副作用有关。并且变量确实是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.

当您从模型中创建对象时,它还没有主键,因为您还没有保存它。但是,在保存时,Django是否必须确保它更新已存在对象的属性?主键是合乎逻辑的,但它也会引导您期望更新其他属性。

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.

一个例子是Django的unicode处理。无论你把什么字符集都放到数据库中:Django一旦你再次出来就会给你unicode。但是如果你创建一个对象(带有一些非unicode属性)并保存它,Django应该修改你现有对象的那个文本属性吗?这已经听起来有点危险了。这可能是(可能)为什么Django不会对您要求它存储在数据库中的对象进行任何动态更新。

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.

从数据库重新加载对象为您提供了一个完美的对象,其中包含所有设置,但它也使您的变量指向不同的对象。因此,如果您已经在“旧”Movie对象上给了Rating一个指针,那么这对您的示例没有帮助。

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.

Hedde提到的Movie.objects.create(title =“The Hobbit”)就是这里的伎俩。它从数据库返回一个电影对象,因此它已经有了一个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.)

(当我新创建的对象没有输出unicode时,我的数据库中的对象和对象之间的区别也出现了问题。我在博客上的解释与上面相同,但措辞有点不同。)

#4


0  

My opinion is that after you call the save() method on your hobbit object, that object is saved. but the local reference that is present in your my_rating object doesnt really know it has to update itself with values that are present in the database.

我的观点是,在你的hobbit对象上调用save()方法后,该对象被保存。但是my_rating对象中存在的本地引用并不真正知道它必须使用数据库中存在的值更新自身。

So when you call my_rating.movie.id django doesnt recognize the need for a db query on the movie object again and hence you get None, which is the value that the local instance of that object contains.

因此,当您调用my_rating.movi​​e.id时,django无法再次识别对电影对象的db查询,因此您将获得None,即该对象的本地实例所包含的值。

but my_rating.movie_id doesnt depend on what data is present on your local instances - that is an explicit way of asking django to look into the database and see what information is there through the foreign key relationship.

但是my_rating.movi​​e_id并不依赖于本地实例上存在的数据 - 这是一种明确的方式,要求django查看数据库,并通过外键关系查看哪些信息。

#5


0  

Just to complete, as I am not able to comment...

刚刚完成,因为我无法发表评论......

You may also (but rather not in this case) be willing to change the behaviour on the database side. This may be useful for running some tests that may lead to similar problems (since they are done in a commit and rollbacked). Sometimes it may be better to use this hacky command in order to keep the tests as close as possible to the real behaviour of the app rather than packing them in a TransactionalTestCase :

您也可以(但不是在这种情况下)愿意更改数据库端的行为。这可能对运行某些可能导致类似问题的测试很有用(因为它们是在提交和回滚中完成的)。有时候使用这个hacky命令可能会更好,以使测试尽可能接近应用程序的真实行为,而不是将它们打包在TransactionalTestCase中:

It has to do with the properties of the constraints... Executing the following SQL command will also solve the problem (PostgreSQL only):

它与约束的属性有关...执行以下SQL命令也将解决问题(仅限PostgreSQL):

SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;

#1


9  

Looking in the Django source, the answer lies in some of the magic Django uses to provide its nice API.

看看Django源代码,答案在于Django用来提供其优秀API的一些神奇功能。

When you instantiate a Rating object, Django sets (though with some more indirection to make this generic) self.movie to the_hobbit. However, self.movie isn'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 property movie_id instead of movie, since it's a ForeignKey field. However, since the_hobbit.pk is None, it just sets movie to the_hobbit instead. Once you try to save your rating, it tries to look up movie_id again, which fails (it doesn't even try to look at movie.)

当你实例化一个Rating对象时,Django设置(虽然有一些更多的间接使这个通用)self.movi​​e到the_hobbit。但是,self.movi​​e不是常规属性,而是通过__set__设置。 __set__方法(上面链接)查看值(the_hobbit)并尝试设置属性movie_id而不是movie,因为它是ForeignKey字段。但是,由于the_hobbit.pk为None,它只是将电影设置为the_hobbit。一旦你试图保存你的评级,它会再次尝试查看movie_id,这会失败(它甚至不会尝试查看电影。)

Interestingly, it seems this behaviour is changing in Django 1.5.

有趣的是,似乎这种行为在Django 1.5中正在发生变化。

Instead of

代替

setattr(value, self.related.field.attname, getattr(
    instance, self.related.field.rel.get_related_field().attname))
# "self.movie_id = movie.pk"

it now does

现在呢

    related_pk = getattr(instance, self.related.field.rel.get_related_field().attname)
    if related_pk is None:
        raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
                            (value, instance._meta.object_name))

which in your case would result in a more helpful error message.

在您的情况下,这将导致更有用的错误消息。

#2


10  

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:

在模型类上添加classmethod:

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

来源: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.

分配the_hobbit时,您将分配一个Movie实例,因此不会访问数据库。一旦调用'save',数据库就会填满,但是你的变量仍然指向内存中的对象,而不知道数据库的突然变化。

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()

#3


10  

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

主要问题与想要或不想要的副作用有关。并且变量确实是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.

当您从模型中创建对象时,它还没有主键,因为您还没有保存它。但是,在保存时,Django是否必须确保它更新已存在对象的属性?主键是合乎逻辑的,但它也会引导您期望更新其他属性。

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.

一个例子是Django的unicode处理。无论你把什么字符集都放到数据库中:Django一旦你再次出来就会给你unicode。但是如果你创建一个对象(带有一些非unicode属性)并保存它,Django应该修改你现有对象的那个文本属性吗?这已经听起来有点危险了。这可能是(可能)为什么Django不会对您要求它存储在数据库中的对象进行任何动态更新。

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.

从数据库重新加载对象为您提供了一个完美的对象,其中包含所有设置,但它也使您的变量指向不同的对象。因此,如果您已经在“旧”Movie对象上给了Rating一个指针,那么这对您的示例没有帮助。

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.

Hedde提到的Movie.objects.create(title =“The Hobbit”)就是这里的伎俩。它从数据库返回一个电影对象,因此它已经有了一个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.)

(当我新创建的对象没有输出unicode时,我的数据库中的对象和对象之间的区别也出现了问题。我在博客上的解释与上面相同,但措辞有点不同。)

#4


0  

My opinion is that after you call the save() method on your hobbit object, that object is saved. but the local reference that is present in your my_rating object doesnt really know it has to update itself with values that are present in the database.

我的观点是,在你的hobbit对象上调用save()方法后,该对象被保存。但是my_rating对象中存在的本地引用并不真正知道它必须使用数据库中存在的值更新自身。

So when you call my_rating.movie.id django doesnt recognize the need for a db query on the movie object again and hence you get None, which is the value that the local instance of that object contains.

因此,当您调用my_rating.movi​​e.id时,django无法再次识别对电影对象的db查询,因此您将获得None,即该对象的本地实例所包含的值。

but my_rating.movie_id doesnt depend on what data is present on your local instances - that is an explicit way of asking django to look into the database and see what information is there through the foreign key relationship.

但是my_rating.movi​​e_id并不依赖于本地实例上存在的数据 - 这是一种明确的方式,要求django查看数据库,并通过外键关系查看哪些信息。

#5


0  

Just to complete, as I am not able to comment...

刚刚完成,因为我无法发表评论......

You may also (but rather not in this case) be willing to change the behaviour on the database side. This may be useful for running some tests that may lead to similar problems (since they are done in a commit and rollbacked). Sometimes it may be better to use this hacky command in order to keep the tests as close as possible to the real behaviour of the app rather than packing them in a TransactionalTestCase :

您也可以(但不是在这种情况下)愿意更改数据库端的行为。这可能对运行某些可能导致类似问题的测试很有用(因为它们是在提交和回滚中完成的)。有时候使用这个hacky命令可能会更好,以使测试尽可能接近应用程序的真实行为,而不是将它们打包在TransactionalTestCase中:

It has to do with the properties of the constraints... Executing the following SQL command will also solve the problem (PostgreSQL only):

它与约束的属性有关...执行以下SQL命令也将解决问题(仅限PostgreSQL):

SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;