Problème avec la mise à jour ManyToMany Relations pas immédiatement après sauver
-
20-09-2019 - |
Question
Je vais avoir des problèmes avec ManyToMany Les relations qui ne sont pas mise à jour
dans un modèle quand je l'enregistrer (via l'admin) et essayez d'utiliser la nouvelle valeur dans un
fonction attachée au signal de post_save
ou dans le save_model
de
le AdminModel
associé.
J'ai essayé de recharger l'objet dans ces fonctions à l'aide du
obtenir la fonction avec l'id .. mais il a encore les anciennes valeurs.
Est-ce un problème de transaction? Y at-il un signal jeté lorsque le transaction se termine?
Merci,
La solution
Lorsque vous enregistrez un modèle via un formulaire d'administration, il n'est pas une transaction atomique. L'objet principal est enregistré d'abord (pour vous assurer qu'il a un PK), le M2M est effacé et les nouvelles valeurs définies à tout ce qui est sorti de la forme. Donc, si vous êtes dans la sauvegarde () de l'objet principal, vous êtes dans une fenêtre d'opportunité où le M2M n'a pas encore été mis à jour. En fait, si vous essayez pour faire quelque chose au M2M, le changement s'effacé par le clair (). Je suis tombé sur ce il y a environ un an.
Le code a changé quelque peu les jours de pré-réusiner ORM, mais il se résume à code dans django.db.models.fields.ManyRelatedObjectsDescriptor
et ReverseManyRelatedObjectsDescriptor
. Regardez leurs méthodes __ de __set () et vous verrez que manager.clear(); manager.add(*value)
claire et complète () nettoie toutes les références M2M pour l'objet courant principal dans ce tableau. Le module complémentaire () définit ensuite les nouvelles valeurs.
Pour répondre à votre question. Oui, il est une question de transaction
Y at-il un signal lancé lorsque la transaction se termine? Rien d'officiel, mais lu sur:
Il y avait un fil lié il y a quelques mois et MonkeyPatching était une méthode proposée. Grégoire a affiché un pour cette monkey-patch. Je ne l'ai pas essayé, mais il semble que cela devrait fonctionner.
Autres conseils
Lorsque vous essayez d'accéder aux champs de ManyToMany dans le signal post_save du modèle, les objets connexes ont déjà été supprimés et ne seront pas ajoutés à nouveau jusqu'à ce que le signal est terminé.
Pour accéder à ces données, vous devez lier dans la méthode save_related dans votre ModelAdmin. Malheureusement, vous aurez également à inclure le code dans le signal post_save pour les demandes non-administrateurs qui nécessitent la personnalisation.
Exemple:
# admin.py
Class GroupAdmin(admin.ModelAdmin):
...
def save_related(self, request, form, formsets, change):
super(GroupAdmin, self).save_related(request, form, formsets, change)
# do something with the manytomany data from the admin
form.instance.users.add(some_user)
Ensuite, dans vos signaux, vous pouvez apporter les mêmes modifications que vous souhaitez exécuter sur une sauvegarde:
# signals.py
@receiver(post_save, sender=Group)
def group_post_save(sender, instance, created, **kwargs):
# do somethign with the manytomany data from non-admin
instance.users.add(some_user)
# note that instance.users.all() will be empty from the admin: []
J'ai une solution générale à ce qui semble un peu plus propre que le singe-patcher le noyau ou même en utilisant le céleri (même si je suis sûr que quelqu'un pourrait trouver des domaines où elle échoue). Fondamentalement, j'ajoute une méthode propre () dans l'administration de la forme qui a les relations m2m, et définir les relations d'instance à la version cleaned_data. Cela rend les données correctes disponibles à la méthode par exemple économisons, même si ce n'est pas « sur les livres » encore. Essayez et voir comment il va:
def clean(self, *args, **kwargs):
# ... actual cleaning here
# then find the m2m fields and copy from cleaned_data to the instance
for f in self.instance._meta.get_all_field_names():
if f in self.cleaned_data:
field = self.instance._meta.get_field_by_name(f)[0]
if isinstance(field, ManyToManyField):
setattr(self.instance,f,self.cleaned_data[f])
Voir http: / /gterzian.github.io/Django-Cookbook/signals/2013/09/07/manipulating-m2m-with-signals.html
problème: Lorsque vous manipulez le m2m d'un modèle à l'intérieur d'un poste ou d'un récepteur de signaux pre_save, vos modifications s'anéanties dans la « compensation » ultérieure du m2m par Django.
solution: En vous publiez ou gestionnaire de signaux pre_save, enregistrer un autre gestionnaire au signal m2m_changed sur le modèle intermédiaire m2m du modèle dont m2m vous souhaitez mettre à jour.
S'il vous plaît noter que ce second gestionnaire recevra plusieurs signaux m2m_changed, et il est essentiel de tester la valeur des arguments « action » passé avec eux.
Dans ce second gestionnaire, vérifiez l'action "post_clear. Lorsque vous recevez un signal avec l'action post_clear, le m2m a été effacée par Django et vous avez une chance de manipuler avec succès.
un exemple:
def save_handler(sender, instance, *args, **kwargs):
m2m_changed.connect(m2m_handler, sender=sender.m2mfield.through, weak=False)
def m2m_handler(sender, instance, action, *args, **kwargs):
if action =='post_clear':
succesfully_manipulate_m2m(instance)
pre_save.connect(save_handler, sender=YouModel, weak=False)
voir https://docs.djangoproject.com/en/ 1,5 / ref / signaux / # m2m-changé
Vous pouvez trouver plus d'informations sur ce sujet:? signaux Django ManyToMany
L'une des solutions pour mettre à jour m2m, ainsi que la mise à jour de vos modèles.
Django 1.11 and higher
Tout d'abord, toutes les demandes via le panneau d'administration sont atomiques. Vous pouvez regarder ModelAdmin:
@csrf_protect_m def changeform_view(self, request, object_id=None, form_url='', extra_context=None): with transaction.atomic(using=router.db_for_write(self.model)): return self._changeform_view(request, object_id, form_url, extra_context) @csrf_protect_m def delete_view(self, request, object_id, extra_context=None): with transaction.atomic(using=router.db_for_write(self.model)): return self._delete_view(request, object_id, extra_context)
Le comportement que vous pouvez observer lors de la mise à jour, lorsque des modifications que vous avez faites avec des enregistrements m2m ne sont pas sauvegardés, même après que vous les avez faites dans une méthode de sauvegarde de vos modèles ou dans un signal, ne se produit que parce que la forme m2m réécrit tous les enregistrements après que l'objet principal est mis à jour.
C'est pourquoi, étape par étape:
-
Le but principal est mis à jour.
-
Votre code (dans une méthode de sauvegarde ou dans un signal) a apporté des modifications (vous pouvez les regarder, il suffit de mettre un point d'arrêt dans ModelAdmin):
def save_related(self, request, form, formsets, change): breakpoint() form.save_m2m() for formset in formsets: self.save_formset(request, form, formset, change=change)
- form.save_m2m () prend toutes les valeurs m2m qui ont été placés sur une page (grosso modo) et remplacent tous les enregistrements m2m par un gestionnaire associé. Voilà pourquoi vous ne pouvez pas voir vos modifications à la fin d'une transaction.
Il y a une solution: apportez vos modifications avec m2m via
transaction.on_commit
. transaction.on_commit fera vos modifications après form.save_m2m () lorsque la transaction est validée.
Malheureusement, l'inconvénient de cette solution -. Avec vos modifications m2m seront exécutées dans une transaction séparée