Duplication d'instances de modèles et de leurs objets connexes dans Django / algorithme pour dupliquer un objet de manière récursive
j'ai des modèles pour Books
, Chapters
et Pages
. Ils sont tous écrits par un User
:
from django.db import models
class Book(models.Model)
author = models.ForeignKey('auth.User')
class Chapter(models.Model)
author = models.ForeignKey('auth.User')
book = models.ForeignKey(Book)
class Page(models.Model)
author = models.ForeignKey('auth.User')
book = models.ForeignKey(Book)
chapter = models.ForeignKey(Chapter)
ce que j'aimerais faire, c'est dupliquer un Book
existant et mettre à jour son User
à quelqu'un d'autre. Le problème est que je voudrais aussi dupliquer toutes les instances de modèles reliées au Book
- tout ce que c'est Chapters
et Pages
aussi bien!
les choses deviennent vraiment délicates quand on regarde un Page
- non seulement le nouveau Pages
devra avoir son champ author
mis à jour, mais il devra aussi pointer vers les nouveaux Chapter
objets!
est-ce que Django soutient une façon de faire cela? À quoi ressemblerait un algorithme générique pour dupliquer un modèle?
Cheers,
John
mise à Jour:
les classes donné ci-dessus sont juste un exemple pour illustrer le problème que j'ai!
10 réponses
cela ne fonctionne plus à Django 1.3 car les objets collectés ont été supprimés. Voir changements" 14507 151950920"
j'ai posté ma solution sur Django Snippets. il est fortement basé sur le code django.db.models.query.CollectedObject
utilisé pour supprimer des objets:
from django.db.models.query import CollectedObjects
from django.db.models.fields.related import ForeignKey
def duplicate(obj, value, field):
"""
Duplicate all related objects of `obj` setting
`field` to `value`. If one of the duplicate
objects has an FK to another duplicate object
update that as well. Return the duplicate copy
of `obj`.
"""
collected_objs = CollectedObjects()
obj._collect_sub_objects(collected_objs)
related_models = collected_objs.keys()
root_obj = None
# Traverse the related models in reverse deletion order.
for model in reversed(related_models):
# Find all FKs on `model` that point to a `related_model`.
fks = []
for f in model._meta.fields:
if isinstance(f, ForeignKey) and f.rel.to in related_models:
fks.append(f)
# Replace each `sub_obj` with a duplicate.
sub_obj = collected_objs[model]
for pk_val, obj in sub_obj.iteritems():
for fk in fks:
fk_value = getattr(obj, "%s_id" % fk.name)
# If this FK has been duplicated then point to the duplicate.
if fk_value in collected_objs[fk.rel.to]:
dupe_obj = collected_objs[fk.rel.to][fk_value]
setattr(obj, fk.name, dupe_obj)
# Duplicate the object and save it.
obj.id = None
setattr(obj, field, value)
obj.save()
if root_obj is None:
root_obj = obj
return root_obj
Voici un moyen facile de copier votre objet.
en gros:
(1) Mettez L'id de votre objet original à None:
book_to_copy.id = None
(2) Modifier l'attribut "author" et sauvegarder le ojbect:
book_to_copy.auteur = new_author
book_to_copy.save ()
(3) Insérer effectuée au lieu de mise à jour
(Il n'a pas d'adresse changement de l'auteur dans la Page--je suis d'accord avec les commentaires au sujet de la restructuration des modèles)
je n'ai pas essayé dans django mais python propriétédeepcopy pourrait travailler pour vous
EDIT:
vous pouvez définir le comportement de copie personnalisé pour vos modèles si vous implémentez des fonctions:
__copy__() and __deepcopy__()
ceci est une édition de http://www.djangosnippets.org/snippets/1282 /
il est maintenant compatible avec le collecteur qui a remplacé CollectedObjects en 1.3.
Je n'ai pas vraiment testé cela trop lourdement, mais je l'ai testé avec un objet avec environ 20.000 sous-objets, mais dans seulement environ trois couches de profondeur de clé étrangère. Utilisez à vos propres risques, bien sûr.
pour le gars ambitieux qui lit ce post, vous devrait envisager sous-classe Collector (ou copier la classe entière pour supprimer cette dépendance sur cette section non publiée de l'API django) à une classe appelée quelque chose comme "DuplicateCollector" et l'écriture A.double méthode qui fonctionne de la même façon à la .supprimer la méthode. cela résoudrait ce problème d'une manière réelle.
from django.db.models.deletion import Collector
from django.db.models.fields.related import ForeignKey
def duplicate(obj, value=None, field=None, duplicate_order=None):
"""
Duplicate all related objects of obj setting
field to value. If one of the duplicate
objects has an FK to another duplicate object
update that as well. Return the duplicate copy
of obj.
duplicate_order is a list of models which specify how
the duplicate objects are saved. For complex objects
this can matter. Check to save if objects are being
saved correctly and if not just pass in related objects
in the order that they should be saved.
"""
collector = Collector({})
collector.collect([obj])
collector.sort()
related_models = collector.data.keys()
data_snapshot = {}
for key in collector.data.keys():
data_snapshot.update({ key: dict(zip([item.pk for item in collector.data[key]], [item for item in collector.data[key]])) })
root_obj = None
# Sometimes it's good enough just to save in reverse deletion order.
if duplicate_order is None:
duplicate_order = reversed(related_models)
for model in duplicate_order:
# Find all FKs on model that point to a related_model.
fks = []
for f in model._meta.fields:
if isinstance(f, ForeignKey) and f.rel.to in related_models:
fks.append(f)
# Replace each `sub_obj` with a duplicate.
if model not in collector.data:
continue
sub_objects = collector.data[model]
for obj in sub_objects:
for fk in fks:
fk_value = getattr(obj, "%s_id" % fk.name)
# If this FK has been duplicated then point to the duplicate.
fk_rel_to = data_snapshot[fk.rel.to]
if fk_value in fk_rel_to:
dupe_obj = fk_rel_to[fk_value]
setattr(obj, fk.name, dupe_obj)
# Duplicate the object and save it.
obj.id = None
if field is not None:
setattr(obj, field, value)
obj.save()
if root_obj is None:
root_obj = obj
return root_obj
EDIT: suppression d'une instruction "print" de débogage.
Dans Django 1.5 ce qui fonctionne pour moi:
thing.id = None
thing.pk = None
thing.save()
L'utilisation du snippet CollectedObjects ci-dessus ne fonctionne plus mais peut être fait avec la modification suivante:
from django.contrib.admin.util import NestedObjects
from django.db import DEFAULT_DB_ALIAS
et
collector = NestedObjects(using=DEFAULT_DB_ALIAS)
au lieu de CollectorObjects
S'il n'y a que quelques copies dans la base de données que vous construisez, j'ai trouvé que vous pouvez juste utiliser le bouton arrière dans l'interface d'administration, changer les champs nécessaires et enregistrer l'instance à nouveau. Cela a fonctionné pour moi dans les cas où, par exemple, j'ai besoin de construire un "gimlet" et un cocktail "vodka gimlet" où la seule différence est de remplacer le nom et un ingrédient. De toute évidence, cela nécessite un peu de prévoyance des données et n'est pas aussi puissant que de passer outre la copie de django/deepcopy - mais il peut faire l'affaire pour certains.
Django a une manière intégrée de dupliquer un objet via l'administrateur-comme répondu ici: dans L'interface D'administration de Django, y a-t-il un moyen de dupliquer un article?
je pense que vous seriez plus heureux avec un modèle de données plus simple, aussi.
est-il vrai qu'une Page se trouve dans un chapitre, mais dans un livre différent?
userMe = User( username="me" )
userYou= User( username="you" )
bookMyA = Book( userMe )
bookYourB = Book( userYou )
chapterA1 = Chapter( book= bookMyA, author=userYou ) # "me" owns the Book, "you" owns the chapter?
chapterB2 = Chapter( book= bookYourB, author=userMe ) # "you" owns the book, "me" owns the chapter?
page1 = Page( book= bookMyA, chapter= chapterB2, author=userMe ) # Book and Author aggree, chapter doesn't?
il semble que votre modèle soit trop complexe.
je pense que vous seriez plus heureux avec quelque chose de plus simple. Je suis juste deviner à cela, puisque je n'ai pas votre savoir totalité du problème.
class Book(models.Model)
name = models.CharField(...)
class Chapter(models.Model)
name = models.CharField(...)
book = models.ForeignKey(Book)
class Page(models.Model)
author = models.ForeignKey('auth.User')
chapter = models.ForeignKey(Chapter)
chaque page a une paternité distincte. Chaque chapitre, puis, a une collection d'auteurs, comme le livre. Maintenant vous pouvez dupliquer le livre, Le chapitre et les Pages, assignant les Pages clonées au nouvel Auteur.
en effet, vous pourriez vouloir avoir une relation de plusieurs à plusieurs entre la Page et le chapitre, vous permettant d'avoir plusieurs copies de juste la Page, sans cloner le livre et le chapitre.
Simple, sans façon générique
les solutions proposées n'ont pas fonctionné pour moi, donc j'ai pris la voie simple, pas intelligente. Ceci n'est utile que pour les cas simples.
pour un modèle ayant la structure suivante
Book
|__ CroppedFace
|__ Photo
|__ AwsReco
|__ AwsLabel
|__ AwsFace
|__ AwsEmotion
ça fonctionne
def duplicate_book(book: Book, new_user: MyUser):
# AwsEmotion, AwsFace, AwsLabel, AwsReco, Photo, CroppedFace, Book
old_cropped_faces = book.croppedface_set.all()
old_photos = book.photo_set.all()
book.pk = None
book.user = new_user
book.save()
for cf in old_cropped_faces:
cf.pk = None
cf.book = book
cf.save()
for photo in old_photos:
photo.pk = None
photo.book = book
photo.save()
if hasattr(photo, 'awsreco'):
reco = photo.awsreco
old_aws_labels = reco.awslabel_set.all()
old_aws_faces = reco.awsface_set.all()
reco.pk = None
reco.photo = photo
reco.save()
for label in old_aws_labels:
label.pk = None
label.reco = reco
label.save()
for face in old_aws_faces:
old_aws_emotions = face.awsemotion_set.all()
face.pk = None
face.reco = reco
face.save()
for emotion in old_aws_emotions:
emotion.pk = None
emotion.aws_face = face
emotion.save()
return book