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!

29
demandé sur Serjik 2009-01-13 00:58:55

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
15
répondu jb. 2011-03-04 16:33:48

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)

9
répondu rprasad 2010-06-25 18:25:24

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__()
8
répondu Sergey Golovchenko 2009-01-13 00:25:40

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.

7
répondu James 2011-05-19 19:51:02

Dans Django 1.5 ce qui fonctionne pour moi:

thing.id = None
thing.pk = None
thing.save()
4
répondu ShawnFumo 2013-05-22 19:28:37

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

4
répondu Paul 2016-01-23 03:46:12

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.

3
répondu pragmar 2010-06-22 13:28:48

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?

3
répondu user1017147 2017-05-23 11:46:46

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.

1
répondu S.Lott 2009-01-12 23:19:07

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
1
répondu maxbellec 2017-02-24 17:50:53