Django 啥时候查找外键的主键?
Posted
技术标签:
【中文标题】Django 啥时候查找外键的主键?【英文标题】:When does Django look up the primary key of foreign keys?Django 什么时候查找外键的主键? 【发布时间】:2012-11-17 20:19:44 【问题描述】:我有两个简单的模型,一个代表电影,另一个代表电影的评分。
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()
我的期望是我能够首先创建一个引用该电影的Movie
和一个Review
,然后将它们都提交到数据库中,只要我首先提交Movie
以便它被赋予一个Review
要引用的主键。
the_hobbit = Movie(title="The Hobbit")
my_rating = Rating(movie=the_hobbit, rating=8.5)
the_hobbit.save()
my_rating.save()
令我惊讶的是,它仍然引发了 IntegrityError
抱怨我试图指定一个空外键,即使 Movie
已经提交并且现在有一个主键。
IntegrityError: null value in column "movie_id" violates not-null constraint
我通过添加一些print
声明确认了这一点:
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
.movie
属性指的是一个Movie
实例,它确实有一个非None
.id
,但.movie_id
保留了None
的值,它在Movie
实例时具有被装箱了。
当我尝试提交 Review
时,我希望 Django 会查找 .movie.id
,但显然它不是这样做的。
一边
在我的例子中,我通过在某些模型上覆盖 .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)
这很 hacky,但它对我有用,所以 我并不是真的在寻找解决这个特定问题的方法。
我正在寻找对 Django 行为的解释。 Django 在什么时候查找外键的主键?请具体;最好参考 Django 源代码。
【问题讨论】:
我不在我的电脑上,但是您是否尝试过通过管理器创建您的序列?Movie.objects.create(title="The Hobbit")
等
@Hedde 感谢您的建议!这确实有效。查看文档,似乎使用管理器会导致立即保存对象,因此这与在创建Review
之前手动调用Movie
上的.save()
相同。 Django 绝对似乎只在创建实例时检查 ID。我希望有人能找到这种行为的准确参考。
我尝试编写一个类装饰器,用检索.field.id
的装饰器替换模型上的所有.field_id
属性。不幸的是,事实证明.field
是一个描述符,它依赖于.field_id
的值才能正常运行。鉴于我对 Django 内部的了解有限,这对我来说太复杂了,无法轻松处理。
ReverseSingleRelatedObjectDescriptor 类型检查相关对象并使其管理器可用作实例上的属性。如果您需要猴子补丁,IMO 退后一步,重新考虑您的目标。很有可能有一条比侵入 Django 内部更明显的途径。但我喜欢你的问题,希望对 Django 哲学有更多经验的人能够阐明这个 +1
不是严格相关但有趣的相关花絮:如果你.save()
一个实例,以便 Django 从数据库中获取一个.id
,即使事务是@987654355,该实例也会保留那个.id
@发生被回滚。 (对我来说,他们将如何做其他事情并不明显;我不是在批评这一点。)下次您尝试 .save()
它时,Django 将使用相同的主键 INSERT
它 - 数据库不会生成一个新的。 (我认为即使在这样的回滚情况下,大多数/所有数据库后端也永远不会生成相同的主键,所以这应该不是问题。)
【参考方案1】:
如文档所述:
关键字参数只是您拥有的字段的名称 在您的模型上定义。请注意,以任何方式实例化模型 触及您的数据库;为此,您需要 save()。
在模型类上添加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")
在自定义管理器上添加方法(通常是首选):
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")
原产地: https://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#creating-objects
当您分配 the_hobbit 时,您正在分配一个 Movie 实例,因此不会访问数据库。一旦你调用“保存”,数据库确实会填满,但是你的变量仍然指向内存中的对象,不知道数据库的突然变化。
也就是说,更改序列的顺序也应该有效地创建对象:
the_hobbit = Movie(title="The Hobbit")
the_hobbit.save()
my_rating = Rating(movie=the_hobbit, rating=8.5)
my_rating.save()
【讨论】:
感谢您的回答,但这并不是我要问的。我可以让 Django 通过按特定顺序创建对象来保存对象,但我对 why 的 details / 它的行为方式更感兴趣。 (这篇文章有一个简化的例子,但在我的实际应用程序中,对象是在不同的位置创建的,不能立即保存。) 当人们开始指向虚拟属性时,人们很容易想到不需要的副作用。您必须要求(核心)开发人员避免讨论,在某些时候这一定是一个设计决定。但是,对于您的具体情况,您可以查看pre_save 信号【参考方案2】:主要问题与是否需要副作用有关。在 Python 中,变量实际上是指向对象的指针。
当您从模型创建对象时,它还没有主键,因为您还没有保存它。但是,保存它时,Django 是否必须确保它更新已存在对象的属性?主键是合乎逻辑的,但它也会导致您期望其他属性被更新。
Django 的 unicode 处理就是一个例子。无论您给输入数据库的文本提供什么字符集:一旦您再次将其取出,Django 就会为您提供 unicode。但是如果你创建一个对象(带有一些非 unicode 属性)并保存它,Django 是否应该修改你现有对象的文本属性?这听起来已经有点危险了。这(可能)是为什么 Django 不对您要求它存储在数据库中的对象进行任何即时更新的原因。
从数据库重新加载对象会给你一个完美的对象,一切都已经设置好了,但它也会让你的变量指向不同的对象。因此,如果您已经在“旧”电影对象上为 Rating 提供了一个指针,那么这对您的示例没有帮助。
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()
(当我新创建的对象没有输出 unicode 时,我的对象和数据库中的对象之间的差异也有问题。我在博客上放的 explanation 与上面相同,但措辞是有点不同。)
【讨论】:
听起来像是一个合理的原因 +1 +1 我认为这是问题的正确答案,因为它回答了理论方面【参考方案3】:查看Django source,答案在于 Django 用来提供其出色 API 的一些魔法。
当你实例化一个Rating
对象时,Django 将(虽然为了使这个通用的更多间接)设置为self.movie
到the_hobbit
。但是,self.movie
不是常规属性,而是通过__set__
设置的。 __set__
方法(上面链接)查看值(the_hobbit
)并尝试设置属性movie_id
而不是movie
,因为它是一个ForeignKey
字段。然而,由于the_hobbit.pk
是无,它只是将movie
设置为the_hobbit
。一旦您尝试保存评分,它就会再次尝试查找 movie_id
,但失败了(它甚至不会尝试查看 movie
。)
有趣的是,Django 1.5 中的这种行为似乎正在发生变化。
代替
setattr(value, self.related.field.attname, getattr(
instance, self.related.field.rel.get_related_field().attname))
# "self.movie_id = movie.pk"
现在可以了
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))
在您的情况下,这会导致更有用的错误消息。
【讨论】:
【参考方案4】:我的观点是,在您对 hobbit 对象调用 save() 方法后,该对象就会被保存。但是您的 my_rating 对象中存在的本地引用并不真正知道它必须使用数据库中存在的值来更新自己。
因此,当您调用 my_rating.movie.id 时,django 无法识别再次对电影对象进行 db 查询的需要,因此您会得到 None,这是值该对象的本地实例包含。
但 my_rating.movie_id 并不取决于本地实例上存在哪些数据 - 这是要求 django 查看数据库并通过外键关系查看存在哪些信息的明确方式.
【讨论】:
【参考方案5】:只是为了完成,因为我无法发表评论......
您也可能(但不是在这种情况下)愿意更改数据库端的行为。这可能对运行一些可能导致类似问题的测试很有用(因为它们是在提交和回滚中完成的)。有时最好使用这个 hacky 命令来使测试尽可能接近应用程序的真实行为,而不是将它们打包在 TransactionalTestCase 中:
这与约束的属性有关......执行以下SQL命令也将解决问题(仅限PostgreSQL):
SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;
【讨论】:
以上是关于Django 啥时候查找外键的主键?的主要内容,如果未能解决你的问题,请参考以下文章