22

我有两个简单的模型,一个代表电影,另一个代表电影的评分。

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实例时所具有的值。

.movie.id当我尝试提交时,我希望 Django 抬头Review,但显然这不是它正在做的事情。


在旁边

就我而言,我通过在某些模型上覆盖该方法来处理此行为,.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 源代码。

4

5 回答 5

10

如文档所述:

关键字参数只是您在模型上定义的字段的名称。请注意,实例化模型绝不会触及您的数据库;为此,您需要保存()。

在模型类上添加一个类方法:

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()
于 2012-11-29T18:30:56.987 回答
10

主要问题与是否需要的副作用有关。并且变量实际上是指向 Python 中对象的指针。

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

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

从数据库重新加载对象会给你一个完美的对象,一切都设置好了,但它也让你的变量指向不同的对象。因此,如果您已经在“旧”电影对象上为 Rating 提供了一个指针,那么这对您的示例没有帮助。

HeddeMovie.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 时,我的对象和数据库中的对象之间的差异也有问题。我在博客上的解释与上面相同,但措辞有点不同。)

于 2012-12-04T11:15:30.583 回答
9

查看Django 源代码,答案在于 Django 用来提供其漂亮 API 的一些魔法。

当你实例化一个Rating对象时,Django 设置(虽然有一些更多的间接使这个通用)self.moviethe_hobbit. 但是,self.movie不是常规属性,而是通过__set__. 该__set__方法(上面链接)查看值 ( the_hobbit) 并尝试设置属性movie_id而不是movie,因为它是一个ForeignKey字段。但是,由于the_hobbit.pk是 None,它只是设置moviethe_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))

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

于 2012-12-10T12:13:21.613 回答
0

只是为了完成,因为我无法发表评论......

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

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

SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;
于 2012-12-04T15:24:01.040 回答
0

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

因此,当您调用my_rating.movi​​e.id时, django 没有认识到需要再次对电影对象进行数据库查询,因此您得到None,这是该对象的本地实例包含的值。

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

于 2012-12-05T13:16:51.067 回答