Duplicação de instâncias de modelo e seus objetos relacionados em Django / Algoritmo para recusrively duplicar um objeto

votos
29

Eu tenho modelos para Books, Chapterse Pages. Eles são todos escritos por um 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)

O que eu gostaria de fazer é duplicar um existente Booke atualizá-lo é Usera outra pessoa. A ruga é que eu também gostaria de duplicar todas as instâncias modelo relacionada com a Book- tudo o que é Chapterse Pagescomo bem!

As coisas ficam realmente complicado quando olhada em um Page- não só a nova Pagesnecessidade de ter o seu authorcampo atualizado, mas eles também terão de apontar para os novos Chapterobjetos!

O Django suportar um fora do caminho caixa de fazer isso? O que seria um algoritmo genérico para duplicar um modelo de aparência?

Felicidades,

John


Atualizar:

As aulas dadas acima são apenas um exemplo para ilustrar o problema que estou tendo!

Publicado 12/01/2009 em 22:58
fonte usuário
Em outras línguas...                            


11 respostas

votos
15

Isso não funciona mais no Django 1.3 como CollectedObjects foi removido. Veja changeset 14507

Eu postei a minha solução no Django Snippets. É baseado fortemente no django.db.models.query.CollectedObjectcódigo usado para excluir objetos:

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
Respondeu 14/01/2009 em 01:28
fonte usuário

votos
9

Aqui está uma maneira fácil de copiar seu objeto.

Basicamente:

(1) definir o ID do seu objeto original para Nenhum:

book_to_copy.id = Nenhum

(2) alterar o atributo do 'autor' e salvar o ojbect:

book_to_copy.author = new_author

book_to_copy.save ()

(3) INSERIR IGNORE realizada em vez de ACTUALIZAÇÃO

(Ele não aborda a mudar o autor da página - Eu concordo com os comentários sobre re-estruturação dos modelos)

Respondeu 25/06/2010 em 19:20
fonte usuário

votos
8

Eu não tentei isso no Django, mas de python deepcopy só poderia trabalhar para você

EDITAR:

Você pode definir o comportamento de cópia personalizada para seus modelos se você implementar funções:

__copy__() and __deepcopy__()
Respondeu 12/01/2009 em 23:24
fonte usuário

votos
7

esta é uma edição de http://www.djangosnippets.org/snippets/1282/

Agora é compatível com o coletor que substituiu CollectedObjects em 1,3.

Eu realmente não testar isso muito fortemente, mas não testá-lo com um objeto com cerca de 20.000 sub-objetos, mas em apenas cerca de três camadas de profundidade de chave estrangeira. Use por sua conta e risco, é claro.

Para o indivíduo ambicioso que lê este post, você deve considerar subclasse Collector (ou copiar toda a classe para remover essa dependência nesta seção inédita da API django) para uma classe chamada algo como "DuplicateCollector" e escrever um método .duplicate que funciona de forma semelhante ao método .delete. que resolveria este problema de uma forma real.

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: Removido uma depuração declaração "print".

Respondeu 19/05/2011 em 20:51
fonte usuário

votos
4

Usando os CollectedObjects trecho acima já não funciona, mas pode ser feito com a seguinte modificação:

from django.contrib.admin.util import NestedObjects
from django.db import DEFAULT_DB_ALIAS

e

collector = NestedObjects(using=DEFAULT_DB_ALIAS)

em vez de CollectorObjects

Respondeu 23/01/2016 em 03:46
fonte usuário

votos
4

No Django 1.5 isso funciona para mim:

thing.id = None
thing.pk = None
thing.save()
Respondeu 22/05/2013 em 20:28
fonte usuário

votos
3

Django tem uma maneira interna para duplicar um objeto através do admin - como respondida aqui: Na interface de administração do Django, há uma maneira de duplicar um item?

Respondeu 25/04/2012 em 00:06
fonte usuário

votos
3

Se há apenas um par de cópias no banco de dados que você está construindo, eu encontrei você pode simplesmente usar o botão Voltar na interface de administração, alterar os campos necessários e salvar a instância novamente. Isso tem funcionado para mim nos casos em que, por exemplo, que eu preciso para construir uma "gimlet" e um cocktail "vodka gimlet", onde a única diferença é a substituição do nome e um ingrediente. Obviamente, isso requer um pouco de clarividência dos dados e não é tão poderoso quanto substituindo do Django copy / deepcopy - mas pode fazer o truque para alguns.

Respondeu 22/06/2010 em 14:28
fonte usuário

votos
1

Eu acho que você seria mais feliz com um modelo de dados mais simples, também.

É verdade que uma página está em algum capítulo, mas um livro diferente?

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?

Parece que o seu modelo é muito complexo.

Eu acho que você seria mais feliz com algo mais simples. Eu estou apenas adivinhando isso, já que eu não o sei todo problema.

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)

Cada página tem autoria distinta. Cada capítulo, então, tem uma coleção de autores, como faz o livro. Agora você pode duplicar livro, capítulo e Pages, atribuindo as Páginas clonado para o novo autor.

Na verdade, você pode querer ter um relacionamento muitos-para-muitos entre página e capítulo, permitindo que você tenha várias cópias de apenas a página, sem clonagem livro e capítulo.

Respondeu 13/01/2009 em 00:19
fonte usuário

votos
0

Eu não tive sorte com qualquer uma das respostas aqui com Django 2.1.2 , então eu criei uma forma genérica de executar uma cópia profunda de um modelo de banco de dados que é fortemente baseada nas respostas postadas acima.

As principais diferenças das respostas acima é que ForeignKeyjá não tem um atributo chamado rel, por isso tem de ser alterado para f.remote_field.modeletc.

Além disso, por causa da dificuldade de saber a ordem dos modelos de banco de dados deve ser copiado em, eu criei um sistema de filas simples que empurra o modelo atual para o fim da lista se for sem sucesso copiado. O código é postet abaixo:

import queue
from django.contrib.admin.utils import NestedObjects
from django.db.models.fields.related import ForeignKey

def duplicate(obj, field=None, value=None, max_retries=5):
    # Use the Nested Objects collector to retrieve the related models
    collector = NestedObjects(using='default')
    collector.collect([obj])
    related_models = list(collector.data.keys())

    # Create an object to map old primary keys to new ones
    data_snapshot = {}
    model_queue = queue.Queue()
    for key in related_models:
        data_snapshot.update(
            {key: {item.pk: None for item in collector.data[key]}}
        )
        model_queue.put(key)

    # For each of the models in related models copy their instances
    root_obj = None
    attempt_count = 0
    while not model_queue.empty():
        model = model_queue.get()
        root_obj, success = copy_instances(model, related_models, collector, data_snapshot, root_obj)

        # If the copy is not a success, it probably means that not
        # all the related fields for the model has been copied yet.
        # The current model is therefore pushed to the end of the list to be copied last
        if not success:

            # If the last model is unsuccessful or the number of max retries is reached, raise an error
            if model_queue.empty() or attempt_count > max_retries:
                raise DuplicationError(model)
            model_queue.put(model)
            attempt_count += 1
    return root_obj

def copy_instances(model, related_models, collector, data_snapshot, root_obj):

# Store all foreign keys for the model in a list
fks = []
for f in model._meta.fields:
    if isinstance(f, ForeignKey) and f.remote_field.model in related_models:
        fks.append(f)

# Iterate over the instances of the model
for obj in collector.data[model]:

    # For each of the models foreign keys check if the related object has been copied
    # and if so, assign its personal key to the current objects related field
    for fk in fks:
        pk_field = f"{fk.name}_id"
        fk_value = getattr(obj, pk_field)

        # Fetch the dictionary containing the old ids
        fk_rel_to = data_snapshot[fk.remote_field.model]

        # If the value exists and is in the dictionary assign it to the object
        if fk_value is not None and fk_value in fk_rel_to:
            dupe_pk = fk_rel_to[fk_value]

            # If the desired pk is none it means that the related object has not been copied yet
            # so the function returns unsuccessful
            if dupe_pk is None:
                return root_obj, False

            setattr(obj, pk_field, dupe_pk)

    # Store the old pk and save the object without an id to create a shallow copy of the object
    old_pk = obj.id
    obj.id = None

    if field is not None:
        setattr(obj, field, value)

    obj.save()

    # Store the new id in the data snapshot object for potential use on later objects
    data_snapshot[model][old_pk] = obj.id

    if root_obj is None:
        root_obj = obj

return root_obj, True

Espero que seja de qualquer ajuda :)

O erro duplicação é apenas uma extensão de exceção simples:

class DuplicationError(Exception):
    """
    Is raised when a duplication operation did not succeed

    Attributes:
        model -- The database model that failed
    """

    def __init__(self, model):
        self.error_model = model

    def __str__(self):
        return f'Was not able to duplicate database objects for model {self.error_model}'
Respondeu 15/11/2018 em 17:38
fonte usuário

votos
0

forma genérica não simples

As soluções propostas não funcionou para mim, então eu fui a maneira simples, não inteligente. Isto só é útil para casos simples.

Para um modelo com a seguinte estrutura

Book
 |__ CroppedFace
 |__ Photo
      |__ AwsReco
            |__ AwsLabel
            |__ AwsFace
                  |__ AwsEmotion

isso funciona

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
Respondeu 24/02/2017 em 17:50
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more