Подтвердить что ты не робот

Когда Django просматривает первичный ключ внешних ключей?

У меня есть две простые модели, одна из которых представляет фильм, другая представляет рейтинг для фильма.

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 был упакован.

Я ожидал, что Django будет искать .movie.id, когда я попытаюсь зафиксировать 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)

Это взломанный, но он работает для меня, поэтому я действительно не ищу решение этой конкретной проблемы.


Я ищу объяснение поведения Django. В каких точках Django просматривает первичный ключ для внешних ключей? Пожалуйста, будьте конкретны; лучше всего использовать ссылки на исходный код Django.

4b9b3361

Ответ 1

Глядя в источник Django, ответ кроется в том, что волшебный Django использует, чтобы обеспечить его хороший API.

Когда вы создаете экземпляр объекта Rating, Django устанавливает (хотя и с еще большей косвенностью, чтобы сделать этот общий) self.movie до the_hobbit. Однако self.movie не является регулярным свойством, а скорее устанавливается через __set__. Метод __set__ (связанный выше) смотрит на значение (the_hobbit) и пытается установить свойство movie_id вместо movie, так как это поле ForeignKey. Однако, поскольку the_hobbit.pk - None, он просто устанавливает 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))

который в вашем случае приведет к более полезному сообщению об ошибке.

Ответ 2

Как указано в документах:

Ключевыми аргументами являются просто имена полей youve определенных на вашей модели. Обратите внимание, что создание экземпляра модели никоим образом касается вашей базы данных; для этого вам нужно сохранить().

Добавьте класс класса в класс модели:

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

Когда вы назначаете файл_hobbit, вы назначаете экземпляр фильма, таким образом не попадая в базу данных. Как только вы вызываете "сохранить", база данных заполняется, однако ваша переменная все еще указывает на объект в памяти, не зная о внезапном изменении базы данных.

Тем не менее, изменение порядка вашей последовательности также должно эффективно создавать объекты:

the_hobbit = Movie(title="The Hobbit")
the_hobbit.save()
my_rating = Rating(movie=the_hobbit, rating=8.5)
my_rating.save()

Ответ 3

Основная проблема связана с нежелательными побочными эффектами. И с переменными, которые действительно являются указателями на объекты в Python.

Когда вы создаете объект из модели, он еще не имеет первичного ключа, поскольку вы еще не сохранили его. Но, сохраняя его, должен ли Django обновлять атрибуты уже существующего объекта? Первичный ключ является логичным, но он также заставит вас ожидать обновления других атрибутов.

Примером этого является обработка unicode Django. Какую бы кодировку вы не ввели текст, который вы помещаете в базу данных: Django дает вам unicode, как только вы его получите снова. Но если вы создадите объект (с некоторым атрибутом не-unicode) и сохраните его, должен ли Django изменить этот текстовый атрибут на существующий объект? Это уже звучит немного опаснее. Что (возможно), почему Django не выполняет "на лету" обновление объектов, которые вы просите сохранить в базе данных.

Повторная загрузка объекта из базы данных дает вам идеальный объект со всем установленным, но также делает вашу переменную точкой для другого объекта. Так что это не поможет в вашем примере, если вы уже указали указатель на свой "старый" объект Movie.

Movie.objects.create(title="The Hobbit"), упомянутый Хедедом, - вот трюк. Он возвращает объект фильма из базы данных, поэтому он уже имеет идентификатор.

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. пояснение, которое я положил на моем weblog то же самое, что и выше, но сформулировано несколько иначе.)

Ответ 4

Просто для завершения, поскольку я не могу комментировать...

Вы также можете (но не в этом случае) быть готовы изменить поведение на стороне базы данных. Это может быть полезно для запуска некоторых тестов, которые могут привести к подобным проблемам (поскольку они выполняются в фиксации и откат). Иногда может быть лучше использовать эту хакерскую команду, чтобы максимально приблизить тесты к реальному поведению приложения, а не упаковывать их в TransactionalTestCase:

Он связан со свойствами ограничений... Выполнение следующей команды SQL также решит проблему (только PostgreSQL ):

SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;

Ответ 5

Я считаю, что после вызова метода save() на вашем объекте хоббита этот объект будет сохранен. но локальная ссылка, присутствующая в вашем my_rating объекте, действительно не знает, что она должна обновлять себя значениями, которые присутствуют в базе данных.

Поэтому, когда вы вызываете my_rating.movie.id, django не распознает необходимость в db-запросе для объекта видео снова и, следовательно, вы получаете Нет, что является значением что локальный экземпляр этого объекта содержит.

но my_rating.movie_id не зависит от того, какие данные присутствуют в ваших локальных экземплярах, - это явный способ попросить django заглянуть в базу данных и посмотреть, какая информация существует через отношение внешнего ключа.