Quand Django chercher la clé primaire de clés étrangères?

j'ai deux modèles simples, l'un représentant un film et l'autre représentant une classification pour un film.

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

Je m'attends à pouvoir créer un Movie et Review référencement de ce film puis les engager tous les deux dans la base de données, aussi longtemps que j'ai engagé le Movie d'abord pour qu'on lui donne une clé primaire pour le Review pour faire référence.

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

À ma grande surprise, il pose toujours un IntegrityError se plaindre que j'essayais de spécifiez une clé étrangère nulle, même le Movie avait été commis et avait maintenant une clé primaire.

IntegrityError: null value in column "movie_id" violates not-null constraint

j'ai confirmé par l'ajout de certains print instructions:

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 attribut se réfère à un Movie instance qui a un non -None.id, mais .movie_id tient la valeur None qu'il a eu quand le Movie l'instance a été mise en caisse.

je m'attendais à Django pour rechercher .movie.id quand j'ai essayé d'engager la Review, mais apparemment ce n'est pas ce qu'il fait.


mis de côté

Dans mon cas, j'ai eu ce comportement en remplaçant les .save() méthode sur certains modèles de sorte qu'ils cherchent les clés primaires des clés étrangères à nouveau avant de sauvegarder.

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)

C'est hacky, mais cela fonctionne pour moi, donc je ne suis pas vraiment à la recherche d'une solution à ce problème particulier.


je suis à la recherche d'un explication du comportement de Django. À quel moment Django cherche-t-il la clé primaire pour les clés étrangères? Veuillez être précis; il serait préférable de faire référence au code source de Django.

21
demandé sur Jeremy Banks 2012-11-29 21:19:45

5 réponses

dans le Django source, la réponse se trouve dans une partie de la magie que Django utilise pour fournir son API nice.

quand vous instanciez un Rating object, Django sets (bien qu'avec une certaine Plus indirecte pour rendre ce générique) self.moviethe_hobbit. Cependant, self.movie n'est pas un régulier de propriété, mais est plutôt définie par __set__. __set__ méthode (lien ci-dessus) regarde la valeur (the_hobbit) et tente de définir la propriété movie_id au lieu de movie, depuis c'est un ForeignKey champ. Cependant, depuis the_hobbit.pk le fait pas, elle ne fait qu' moviethe_hobbit à la place. Une fois que vous essayez d'enregistrer votre note, il essaie de rechercher movie_id encore une fois, ce qui échoue (il n'essaie même pas de regarder movie.)

fait intéressant, il semble que ce comportement change en Django 1.5.

au Lieu de

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

il faut

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

ce qui dans votre cas résulterait en une erreur plus utile message.

9
répondu second 2012-12-11 00:31:43

comme indiqué par le docs:

les arguments de mot-clé sont simplement les noms des champs que vous avez défini sur votre modèle. Notez que l'instanciation d'un modèle en aucune façon touche votre base de données; pour cela, vous devez vous enregistrer().

ajouter une méthode de classe sur la classe model:

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

ajouter une méthode sur un gestionnaire personnalisé (habituellement préféré):

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

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

lorsque vous assignez the_hobbit, vous assignez une instance de Movie, donc ne pas frapper la base de données. Une fois que vous appelez "enregistrer" la base de données se remplit, cependant votre variable est toujours pointant vers l'objet en mémoire, sans se rendre compte de la modification soudaine de la base de données.

cela dit, changer l'ordre de vos la séquence devrait également créer des objets:

the_hobbit = Movie(title="The Hobbit")
the_hobbit.save()
my_rating = Rating(movie=the_hobbit, rating=8.5)
my_rating.save()
10
répondu Hedde van der Heide 2012-11-29 18:52:53

le problème principal a à voir avec les effets secondaires qui sont souhaités ou non. Et avec des variables qui sont vraiment des pointeurs vers des objets en Python.

quand vous créez un objet à partir d'un modèle, il n'a pas encore de clé primaire car vous ne l'avez pas encore sauvegardé. Mais, en le sauvegardant, Django devrait-il s'assurer qu'il met à jour les attributs sur l'objet déjà existant? Une clé primaire est logique, mais elle vous amènerait aussi à vous attendre à ce que d'autres attributs soient mis à jour.

Un exemple pour C'est L'unicode de Django. Quel que soit le jeu de caractères que vous donnez au texte que vous mettez dans une base de données: Django vous donne unicode une fois que vous l'obtenez à nouveau. Mais si vous créez un objet (avec un attribut non unicode) et le Sauvegardez, Django devrait-il modifier cet attribut texte sur votre objet existant? Qui sonne déjà un peu plus dangereux. Ce qui explique (probablement) pourquoi Django ne fait aucune mise à jour à la volée des objets que vous lui demandez de stocker dans la base de données.

recharger l'objet à partir de la base de données vous donne un objet parfait avec tout ce qu'il faut, mais elle fait aussi pointer votre variable vers un autre objet. Donc cela n'aiderait pas dans votre exemple au cas où vous avez déjà donné à la notation Un pointeur à votre "ancien" objet de film.

Movie.objects.create(title="The Hobbit") mentionné par Hedde est le truc ici. Elle retourne un objet vidéo à partir de la base de données, donc il a déjà une identité.

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()
explication j'ai mis sur mon weblog est le même que ci-dessus, mais formulé un peu différemment.)

10
répondu Reinout van Rees 2012-12-04 11:15:30

mon opinion est que, après que vous appelez le save () méthode sur votre objet hobbit, cet objet est sauvegardé. mais la référence locale qui est présent dans votre my_rating objet ne sait pas vraiment qu'il doit se mettre à jour avec des valeurs qui sont présentes dans la base de données.

So when you call my_rating.film.id django ne reconnaît pas le besoin d'une requête db sur l'objet film à nouveau et donc vous obtenez Aucun, qui est la valeur que l'instance locale de cet objet contient.

mais my_rating.movie_id ne dépend pas de quelles données sont présentes sur vos instances locales - c'est une façon explicite de demander à django de regarder dans la base de données et de voir quelles informations sont là à travers la relation clé étrangère.

0
répondu Wingston Sharon 2012-12-05 13:16:51

Juste pour compléter, car je ne suis pas en mesure de commenter...

Vous pouvez aussi (mais pas dans ce cas) être disposé à changer le comportement sur la base de données secondaires. Cela peut être utile pour exécuter quelques tests qui peut conduire à des problèmes similaires (puisqu'ils sont fait dans un commit et rollbacked). Parfois, il peut être préférable d'utiliser ce hacky de commande afin de garder les tests d'aussi près que possible du comportement réel de l'application plutôt qu'en les emballant dans un TransactionalTestCase:

Il a à voir avec les propriétés des contraintes... L'exécution de la suite SQL la commande résoudra aussi le problème (PostgreSQL seulement):

SET CONSTRAINTS [ALL / NAME] DEFERRABLE INITIALLY IMMEDIATE;
0
répondu Rmatt 2012-12-11 13:16:20