9
votes

Quelle est la manière standard d'enregistrer quelque chose uniquement si sa clé étrangère existe?

J'utilise Python 3.7 et Django. J'ai le modèle suivant, avec une clé étrangère vers un autre modèle ...

    id = 1
    article = Article.objects.get(pk=id)
    self.assertTrue(article, "A pre-condition of this test is that an article exist with id=" + str(id))
    articlestat = ArticleStat(article=article, elapsed_time_in_seconds=250, hits=25)
    # Delete the article
    article.delete()
    # Attempt to save ArticleStat
    articlestat.save()

Je ne veux enregistrer ceci que si la clé étrangère associée existe, sinon, j'ai remarqué des erreurs. Quelle est la manière standard Django / Python de faire quelque chose comme ça? Je pensais lire que je pourrais utiliser ".exists ()" ( Vérifier si un objet existe a >), mais à la place j'obtiens une erreur

AttributeError: 'Article' object has no attribute 'exists'

Edit: C'est le test unitaire que je dois vérifier ...

class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
    ...

    def save(self, *args, **kwargs):
        if self.article.exists():
            try:
                article_stat = ArticleStat.objects.get(article=self.article, elapsed_time_in_seconds=self.elapsed_time_in_seconds)
                self.id = article_stat.id
                super().save(*args, **kwargs, update_fields=["hits"])
            except ObjectDoesNotExist:
                super().save(*args, **kwargs)


1 commentaires

Le problème ici est que vous appelez le existe () sur une instance et non sur un ensemble de requêtes. Veuillez consulter la documentation et les exemples docs.djangoproject.com/en/2.1/ref/models/querysets/...


7 Réponses :


4
votes

Vous pouvez simplement tester la valeur du champ article . S'il n'est pas défini, je pense que la valeur par défaut est Aucun .

if self.article:  # Value is set

Si vous voulez que ce champ ForeignKey soit facultatif (ce qui ressemble à vous), vous devez pour définir blank = True et null = True sur ce champ. Cela permettra au champ d'être vide (en validation) et définira null sur le champ lorsqu'il n'y est pas.

Comme mentionné dans les commentaires ci-dessous, votre base de données est probablement en vigueur le fait que le champ est obligatoire et refuse de supprimer l'instance article .


8 commentaires

Si vous n'avez pas besoin de l'instance d'article et que vous voulez juste vérifier son existence, je l'affinerais en if self.article_id: . Cela évite alors qu'une requête de base de données soit exécutée si article n'a pas été précédemment récupéré.


@JonahBishop, "if self.article:" ne fonctionne pas. Même si l'article a déjà été supprimé de la base de données, "if self.article" est toujours évalué en une entité non nulle.


@lukewarm, "if self.article_id:" semble également s'évaluer comme vrai même si l'article a été supprimé de la base de données.


Si vous modifiez le modèle pour inclure l'option null = True , cela change-t-il les effets de la suppression de l'instance de modèle?


@dave êtes-vous certain que l'instance de l'article a été supprimée? La plupart des bases de données appliqueront l'intégrité référentielle dans une certaine mesure, ce qui rend ce scénario peu probable si vous venez d'utiliser des champs ForeignKey normaux. Votre réponse à @JonahBishop semble également suggérer que la ligne Article n'a, en fait, pas été supprimée.


@lukewarm, je sais que l'article a été supprimé car lorsque la ligne "super (). save" est exécutée, il en résulte une "ValueError: save () interdit pour empêcher la perte de données due à un objet lié non enregistré" article "." erreur dans mon test unitaire.


@JonahBishop, où est-ce que je mets "null = True"? Pouvez-vous l'énumérer dans votre réponse?


«On_delete» doit également être défini sur «models.SET_NULL».



8
votes

Si vous voulez être sûr que Article existe dans la méthode save de ArticleStat , vous pouvez essayer de l'obtenir à partir de votre base de données et pas seulement testez self.article .

Citant Alex Martelli :

"... La célèbre devise de Grace Murray Hopper," Il est plus facile de demander pardon que la permission ", a de nombreuses applications utiles - en Python, ..."

Je pense qu'utiliser try .. sauf .. else est plus pythonique et je vais faire quelque chose comme ça:

from django.db import models

class ArticleStat(models.Model):
    ...
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name='articlestats'
    )

    def save(self, *args, **kwargs):
        try:
            article = Article.objects.get(pk=self.article_id)
        except Article.DoesNotExist:
            pass
        else:
            try:
                article_stat = ArticleStat.objects.get(
                    article=article,
                    elapsed_time_in_seconds=self.elapsed_time_in_seconds
                )
                self.id = article_stat.id
                super().save(*args, **kwargs, update_fields=["hits"])
            except ArticleStat.DoesNotExist:
                super().save(*args, **kwargs)

p >


6 commentaires

Merci pour cela. Est-ce la manière typique de gérer quelque chose comme ça? Je demande parce que dans le cas rare, l'objet article est supprimé après votre "article = Article.objects.get (pk = self.ar" et avant la ligne "save", cette méthode échouerait, n'est-ce pas?


J'ai essayé d'écrire une réponse basée sur votre question mais je n'ai pas assez de données pour dire si votre solution est la meilleure pour gérer ce comportement dans votre projet particulier. Pour les «événements rares», vous pouvez utiliser des transactions.


Pour l'approche transactionnelle, avez-vous un exemple de ce à quoi cela ressemblerait?


La documentation officielle est le meilleur point de départ docs.djangoproject .com / fr / stable / topics / db / transactions /…


J'ai vu d'autres personnes faire remarquer sur SO que les liens ont tendance à se rompre avec le temps et que les réponses intégrées sont donc préférables dans les questions. Vous n'avez rien à ajouter à votre réponse, mais une sorte de code efficace et infaillible sera certainement ce que je recherche.


J'ai relu votre commentaire et je vous confirme le code proposé dans ma réponse. "Dans les rares cas où l'objet article est supprimé, la méthode get de ArticleStat échouera mais l'exception gérera l'échec. Vous n'avez donc besoin d'aucune transaction comme je l'ai écrit dans les commentaires.



5
votes

Si vous utilisez une base de données relationnelle, des contraintes de clé étrangère seront ajoutées automatiquement après la migration. La méthode save peut ne nécessiter aucune personnalisation.

article = Article.objects.get(id=1)
article.delete()
try:
  ArticleStats.objects.create(article=article, ...)
  print("article stats is created")
except IntegrityError:
  print("article stats is not created")


# Output
article stats is not created

Utilisez le code suivant pour créer ArticleStats

from django.db import IntegrityError
try:
  ArticleStats.objects.create(article=article, ...)
except IntegrityError:
  pass

Si article_id est valide, les objets ArticleStats sont créés sinon IntegrityError est déclenché.

class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name='articlestats'
    )

Remarque: Testé sur MySQL v5.7, Django 1.11


10 commentaires

J'ai écrit un test unitaire pour vérifier cela (inclus maintenant dans la question), mais l'utilisation de votre code entraîne toujours une erreur, "ValueError: save () interdit pour éviter la perte de données due à un objet lié non enregistré 'article'." Peut-être parce que les tests unitaires utilisent SqlLite?


Les contraintes de clé étrangère sont désactivées dans sqlite par défaut, mais ce n'est pas la raison de cette erreur. Indépendamment du backend de la base de données, vous obtiendrez cette erreur. Vérifiez ce lien.


Au lieu d'utiliser .save () si vous utilisez .create (), il ne déclenchera jamais cette erreur. suggestion: articlestat = ArticleStat.objects.create (article = article, elapsed_time_in_seconds = 250, hits = 25) (.create () appelle en interne .save ())


Qu'en est-il des mises à jour d'un objet? J'ai essayé de remplacer "save" par "Update" mais j'ai obtenu une erreur, "AttributeError:" super "object has no attribute" update ""


.update () est défini dans QuerySet et fonctionnera uniquement avec queryset. Par exemple, .update () fonctionne avec ArticleStat.objects.al (). Update (parfois) ou ArticleStat.objects.filter (). Update () Mais ne fonctionnera pas avec ArticleStat.objects.get (id = id) .update () <- cela lancera une erreur.


Les contraintes de clé étrangère sont au niveau de la base de données, donc que vous utilisiez .update () ou .create () ou .save (), vous obtiendrez IntegrityError si l'article.id n'est pas valide.


La "mise à jour" à laquelle je fais référence change ma ligne pour qu'elle ressemble à ceci "super (). Update (* args, ** kwargs, update_fields = [" hits "])"


Je l'ai. La méthode .update () ne fait pas partie de Model. Par conséquent, vous avez cette erreur lorsque vous l'avez remplacé .save () par .update () dans votre code. Je vous recommande de ne pas remplacer la méthode de sauvegarde. Si vous utilisez un SGBDR comme backend de base de données, vous n'aurez jamais de problème avec l'intégrité des données, c'est-à-dire que si un article n'est pas dans la table Article, il ne pourra jamais être référencé dans ArticleStats. En fait, si vous essayez de le faire dans le code de l'application, Django lancera une erreur d'intégrité. 1/2


Non seulement cela, si vous allez directement dans la base de données et essayez de supprimer la table d'article, vous ne pourrez pas le faire. Le logiciel de base de données lancera une erreur indiquant qu'il existe une autre table appelée ArticleStats qui utilise Article. C'est la beauté des bases de données SGBDR. 2/2


Continuons cette discussion dans le chat .



5
votes

Le champ

article de votre modèle ArticleStat n'est pas facultatif. Vous ne pouvez pas enregistrer votre objet ArticleStat sans la ForeignKey dans l'article

Voici un code similaire, item est une ForeignKey dans le modèle Item, et il est obligatoire.

~ interaction = Interaction()
~ interaction.save()
~ IntegrityError: null value in column "item_id" violates not-null constraint

Si j'essaye d'enregistrer un objet d'Interaction depuis le shell sans sélectionner de ForeignKey sur l'élément, je reçois une IntegrityError.

class Interaction(TimeStampedModel, models.Model):
    ...
    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='interactions')
    type = models.IntegerField('Type', choices=TYPE_CHOICES)
    ...

Vous ne 'pas besoin d'une vérification self.article.exists () . Django et Database auront besoin de ce champ et ne vous permettront pas d'enregistrer l'objet sans lui.

Vous devriez lire le champ ForeignKey dans Django Docs


1 commentaires

Salut, je réalise que Django ne me laissera pas enregistrer le champ si l'article n'existe pas - une erreur est générée, ce qui arrête mon programme. Je ne veux pas que mon programme s'arrête. Quelle est la manière typique de Django de gérer ce genre de choses? Il semble que cela doit être une chose assez courante pour les programmeurs et Django a une manière astucieuse de gérer cela.



0
votes

Le code de la dernière ligne articlestat.save () échouera si l'instance de article a été supprimée. Django et la base de données vérifieront automatiquement l'article pour vous si vous utilisez une base de données de relations comme mysql ou sqlite3.

Lors des migrations, une contrainte sera créée. Par exemple:

shell>>> python manage.py sqlmigrate <appname> 0001
CREATE TABLE impress_impress ...
...

ALTER TABLE `impress_impress` ADD CONSTRAINT 
    `impress_impress_target_id_73acd523_fk_account_myuser_id` FOREIGN KEY (`target_id`) 
    REFERENCES `account_myuser` (`id`);

...

Donc, si vous souhaitez enregistrer le articlestat sans article , une erreur sera générée.


1 commentaires

Salut, Oui, je sais qu'une erreur sera générée si l'article n'existe pas - c'est pourquoi je pose ma question. Comment puis-je empêcher un appel à la sauvegarde si l'article n'existe pas ou au moins faire échouer tout le monde correctement? Je ne veux simplement pas qu'une exception soit levée à partir de cette méthode.



3
votes

Comme d'autres réponses l'ont souligné, la clé étrangère Article ForeignKey est requise sur votre modèle ArticleStat et l'enregistrement échouera automatiquement sans une instance d'article valide. La meilleure façon d'échouer correctement avec une entrée non valide consiste à utiliser la validation Form avec Django API Forms . Ou si vous gérez des données sérialisées avec Django Rest Framework, en utilisant un Serializer , qui est l'équivalent de type formulaire pour les données JSON. De cette façon, vous n'avez pas besoin de remplacer la méthode save sauf si vous avez une exigence spécifique.

Jusqu'à présent, personne n'a mentionné l'utilisation correcte de .exists (). Il s'agit d'une méthode d'un queryset pas d'une instance de modèle , c'est pourquoi vous obtenez l'erreur que vous avez mentionnée ci-dessus lorsque vous essayez de l'appliquer à une instance de modèle individuelle avec self.article.exists () . Pour vérifier l'existence d'un objet, utilisez simplement .filter au lieu de .get . Si votre article ( pk = 1 ) existe alors:

article = models.ForeignKey(Article, on_delete=models.SET_NULL, related_name='articlestats', null=True, blank=True)

Renverra un jeu de requête contenant un article:

Article.objects.filter(pk=1).exists()

et

<Queryset: [Article: 1]>

renverra True . Alors que si l'élément n'existe pas, la requête renverra un jeu de requêtes vide et .exists () renverra False , plutôt que de lever une exception (comme une tentative de . get () un objet inexistant fait). Cela s'applique toujours si le pk existait auparavant et a été supprimé.

EDIT: Je viens de remarquer que le on_delete code de votre ArticleStat > le comportement est actuellement défini sur CASCADE . Cela signifie que lorsqu'un article est supprimé, l'articleStat associé est également supprimé. Donc je pense que vous avez dû mal interpréter les erreurs / difficultés que vous avez mentionnées en réponse à la réponse de @ jonah-bishop en essayant if self.article: . Pour une solution minimale, si vous souhaitez toujours conserver l'articleStat après la suppression d'un article, remplacez le mot-clé on_delete par models.SET_NULL et, conformément à la réponse de Jonah, ajoutez le mots-clés supplémentaires null = True, blank = True :

Article.objects.filter(pk=1)

Alors il ne devrait y avoir aucun problème en faisant simplement if self.article: code> pour vérifier une relation d'objet ForeignKey valide. Mais utiliser Forms / Serializers est toujours une meilleure pratique.


7 commentaires

Donc, en ce qui concerne l'écriture de ma méthode "save", où est-ce que je mets la ligne "Article.objects.filter (pk = 1) .exists ()"?


Vous pouvez l'utiliser pour votre vérification de test unitaire, mais il est préférable de ne pas écrire une méthode d'enregistrement personnalisée sur votre modèle juste pour vérifier un article valide. Dans Django, il est fortement recommandé de traiter toutes les entrées utilisateur via un formulaire (ou un sérialiseur) avant de les enregistrer, car non seulement cela valide les données, mais fournit également une protection intégrée contre les intentions malveillantes. Le mot «Form» n'implique pas nécessairement une saisie manuelle de l'utilisateur - un formulaire Django est juste un objet qui valide le contenu. Un ModelForm peut valider tous les champs du modèle avec seulement quelques mots de code.


Vous passez les données d'entrée dans le formulaire puis appelez form.is_valid () , qui interceptera les exceptions et retournera une valeur False s'il y en a, donc vous ne l'enregistrez que si < code> Vrai . Et si vous utilisez les vues basées sur les classes de Django, vous n'avez rien à écrire sauf le formulaire et l'affecter à votre vue. Consultez les documents; il s'agit également d'une procédure pas à pas du bon formulaire .


Merci mais je n'utilise pas de formulaires. Les objets sont créés via un service exécuté en tant que commande Python (parfois les commandes multi-personnes s'exécutent simultanément, ce qui conduit à ces situations étranges où les objets peuvent ne pas exister).


Ils fonctionneront également pour ce cas d'utilisation. Un formulaire / sérialiseur Django est juste un objet qui effectue le dur travail de validation des données et de gestion des exceptions avant une sauvegarde. Sinon, vous devrez attraper l'exception vous-même car vous sélectionnez quelque chose qui n'existe pas ou laissez un champ de base de données obligatoire vide. (Sauf si vous avez accès au pk attendu de l'article ailleurs dans votre processus, auquel cas vous pouvez exécuter un seul filtre d'élément comme ci-dessus.)


(en cas de confusion: vous n'avez pas besoin de code frontal ou de soumission HTTP pour un "Form", vous pouvez simplement le remplir avec vos propres données sur le back-end)


Merci. Donc, si je lis correctement votre réponse modifiée, la chose qui me manque est d'ajouter "if self.article" avant d'enregistrer? Malheureusement, cela ne fonctionne pas. Je remarque que "if self.article" renvoie true même s'il est exécuté directement après une instruction "article.delete ()". Si je viens de supprimer l'article, il devrait renvoyer false, non? Au moins, j'aimerais une sorte de test pour me dire que l'article n'existe plus.



0
votes

Vous pouvez appeler .full_clean () avant .save()

from django.core.exceptions import ValidationError

class ArticleStat(models.Model):
    #...
    def save(self, *args, **kwargs):
        try:
            self.full_clean()
        except ValidationError as e:
            # dont save
            # Do something based on the errors contained in e.message_dict.
            # Display them to a user, or handle them programmatically.
            pass
        else:
            super().save(*args, **kwargs)


4 commentaires

Salut, ça ne marche pas. Lorsque j'appelle la méthode "save" dans le but de mettre à jour un objet existant, le ValidationError est appelé avec l'erreur, '[' Article stat with this Article and Elapsed time in seconds exists. ']}'


Recherchez les erreurs dans votre code, full_clean sait comment gérer les opérations de mise à jour.


Ma mise à jour d'un code objet existant fonctionnait bien, mais lorsque j'ai ajouté le truc "self.full_clean ()", j'ai eu l'erreur dans mon commentaire précédent. J'ai une contrainte unique sur les deux champs mentionnés dans l'erreur. Peut-être que full_clean ne fonctionne pas avec des contraintes uniques pour les mises à jour?


Lisez la documentation , vérifiez le code source et peut-être que vous verrez qu'il prend en charge mettre à jour les opérations et trouvera l'erreur dans votre code.