Django клонировать рекурсивные объекты - PullRequest
2 голосов
/ 04 мая 2020

ранее у меня была проблема, когда я хочу рекурсивно клонировать объекты. Я знаю, что простой способ клонирования объекта таков:

obj = Foo.objects.get(pk=<some_existing_pk>)
obj.pk = None
obj.save()

Но я хочу сделать больше глубины . Например, у меня есть models.py

class Post(TimeStampedModel):
    author = models.ForeignKey(User, related_name='posts',
                               on_delete=models.CASCADE)
    title = models.CharField(_('Title'), max_length=200)
    content = models.TextField(_('Content'))

    ...


class Comment(TimeStampedModel):
    author = models.ForeignKey(User, related_name='comments',
                               on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    comment = models.TextField(_('Comment'))

    ...


class CommentAttribute(TimeStampedModel):
    comment = models.OneToOneField(Comment, related_name='comment_attribute',
                                   on_delete=models.CASCADE)
    is_bookmark = models.BooleanField(default=False)

    ...


class PostComment(TimeStampedModel):
    post = models.ForeignKey(Post, related_name='post_comments',
                             on_delete=models.CASCADE)
    comments = models.ManyToManyField(Comment)

    ...

Когда я клонирую родительский объект из Post, дочерние объекты, такие как Comment, CommentAttribute и PostComment, также будут клонированы следуя новым клонированным Post объектам. Дочерние модели динамически . Итак, я хочу упростить его, создав инструмент, подобный клонеру объектов.

Этот фрагмент ниже - то, что я сделал;

from django.db.utils import IntegrityError


class ObjectCloner(object):
    """
    [1]. The simple way with global configuration:
    >>> cloner = ObjectCloner()
    >>> cloner.set_objects = [obj1, obj2]   # or can be queryset
    >>> cloner.include_childs = True
    >>> cloner.max_clones = 1
    >>> cloner.execute()

    [2]. Clone the objects with custom configuration per-each objects.
    >>> cloner = ObjectCloner()
    >>> cloner.set_objects = [
        {
            'object': obj1,
            'include_childs': True,
            'max_clones': 2
        },
        {
            'object': obj2,
            'include_childs': False,
            'max_clones': 1
        }
    ]
    >>> cloner.execute()
    """
    set_objects = []            # list/queryset of objects to clone.
    include_childs = True       # include all their childs or not.
    max_clones = 1              # maximum clone per-objects.

    def clone_object(self, object):
        """
        function to clone the object.
        :param `object` is an object to clone, e.g: <Post: object(1)>
        :return new object.
        """
        try:
            object.pk = None
            object.save()
            return object
        except IntegrityError:
            return None

    def clone_childs(self, object):
        """
        function to clone all childs of current `object`.
        :param `object` is a cloned parent object, e.g: <Post: object(1)>
        :return
        """
        # bypass the none object.
        if object is None:
            return

        # find the related objects contains with this current object.
        # e.g: (<ManyToOneRel: app.comment>,)
        related_objects = object._meta.related_objects

        if len(related_objects) > 0:
            for relation in related_objects:
                # find the related field name in the child object, e.g: 'post'
                remote_field_name = relation.remote_field.name

                # find all childs who have the same parent.
                # e.g: childs = Comment.objects.filter(post=object)
                childs = relation.related_model.objects.all()

                for old_child in childs:
                    new_child = self.clone_object(old_child)

                    if new_child is not None:
                        # FIXME: When the child field as M2M field, we gote this error.
                        # "TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use comments.set() instead."
                        # how can I clone that M2M values?
                        setattr(new_child, remote_field_name, object)
                        new_child.save()

                    self.clone_childs(new_child)
        return

    def execute(self):
        include_childs = self.include_childs
        max_clones = self.max_clones
        new_objects = []

        for old_object in self.set_objects:
            # custom per-each objects by using dict {}.
            if isinstance(old_object, dict):
                include_childs = old_object.get('include_childs', True)
                max_clones = old_object.get('max_clones', 1)
                old_object = old_object.get('object')  # assigned as object or None.

            for _ in range(max_clones):
                new_object = self.clone_object(old_object)
                if new_object is not None:
                    if include_childs:
                        self.clone_childs(new_object)
                    new_objects.append(new_object)

        return new_objects

Но проблема в том, что дочернее поле как поле M2M, мы получаем эту ошибку.

>>> cloner.set_objects = [post]
>>> cloner.execute()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 114, in execute
    self.clone_childs(new_object)
  File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 79, in clone_childs
    self.clone_childs(new_child)
  File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 76, in clone_childs
    setattr(new_child, remote_field_name, object)
  File "/home/agus/envs/env-django-cloner/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 546, in __set__
    % self._get_set_deprecation_msg_params(),
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use comments.set() instead.
>>> 

Ошибка, полученная от setattr(...), и "Использовать comments.set () вместо" , но я все еще путаю, как обновить значение m2m?

new_child = self.clone_object(old_child)

if new_child is not None:
    setattr(new_child, remote_field_name, object)
    new_child.save()

Я также пробовал использовать этот фрагмент ниже, но все еще есть ошибка. Клонированных объектов m2m много и они не заполнены значениями m2m.

if new_child is not None:
    # check the object_type
    object_type = getattr(new_child, remote_field_name)

    if hasattr(object_type, 'pk'):
        # this mean is `object_type` as real object.
        # so, we can directly use the `setattr(...)`
        # to update the old relation value with new relation value.
        setattr(new_child, remote_field_name, object)

    elif hasattr(object_type, '_queryset_class'):
        # this mean is `object_type` as m2m queryset (ManyRelatedManager).
        # django.db.models.fields.related_descriptors.\
        # create_forward_many_to_many_manager.<locals>.ManyRelatedManager

        # check the old m2m values, and assign into new object.
        # FIXME: IN THIS CASE STILL GOT AN ERROR
        old_m2m_values = getattr(old_child, remote_field_name).all()
        object_type.add(*old_m2m_values)

    new_child.save()

Ответы [ 4 ]

0 голосов
/ 11 мая 2020

Я пытался решить эту интересную проблему с помощью некоторого рабочего кода ... который был сложнее, чем я думал вначале!

Я отошел от вашего исходного решения, поскольку у меня возникли некоторые трудности в следовании логике ObjectCloner c .

Самое простое решение, которое я могу придумать, дано ниже; вместо использования класса я выбрал одну вспомогательную функцию clone_object () , которая работает с одним объектом.

Конечно, вы можете использовать вторую функцию для работы со списком объектов или наборов запросов, отсканировав последовательность и вызвав clone_object () несколько раз.

def clone_object(obj, attrs={}):

    # we start by building a "flat" clone
    clone = obj._meta.model.objects.get(pk=obj.pk)
    clone.pk = None

    # if caller specified some attributes to be overridden, 
    # use them
    for key, value in attrs.items():
        setattr(clone, key, value)

    # save the partial clone to have a valid ID assigned
    clone.save()

    # Scan field to further investigate relations
    fields = clone._meta.get_fields()
    for field in fields:

        # Manage M2M fields by replicating all related records 
        # found on parent "obj" into "clone"
        if not field.auto_created and field.many_to_many:
            for row in getattr(obj, field.name).all():
                getattr(clone, field.name).add(row)

        # Manage 1-N and 1-1 relations by cloning child objects
        if field.auto_created and field.is_relation:
            if field.many_to_many:
                # do nothing
                pass
            else:
                # provide "clone" object to replace "obj" 
                # on remote field
                attrs = {
                    field.remote_field.name: clone
                }
                children = field.related_model.objects.filter(**{field.remote_field.name: obj})
                for child in children:
                    clone_object(child, attrs)

    return clone

A PO C пример проекта, протестированный с Python 3.7.6 и Django 3.0.6, был сохранен в репозитории c publi на github:

https://github.com/morlandi/test-django-clone

0 голосов
/ 06 мая 2020

Первые несколько проблем:

  • related_objects ограничен, он возвращает только обратные отношения - если дочерний элемент имеет внешний ключ к этому родительскому элементу, а не внешние ключи этого родительского элемента к дочернему. Хотя это может быть правильным подходом - сверху донизу - в действительности отношения могут быть организованы по-другому, или у ребенка может быть другой внешний ключ к другой модели - который также может потребоваться клонировать. Необходимо просмотреть все поля и отношения.

Не говоря уже о Django рекомендация:

(related_objects - ) Только для частного API использоваться самой Django; get_fields () в сочетании с фильтрацией свойств полей представляет собой API publi c для получения этого списка полей.


Может предложить несколько иной подход.

Использовать get_fields() вместо related_objects.

fields = object._meta.get_fields() вернет список всех полей модели - определенных как для самой модели, так и для forward / поля обратного доступа, автоматически добавляются django (подобно тем, которые возвращаются related_objects).

Этот список можно отфильтровать, чтобы получить только обязательные поля:

  • field.is_relation - будет True для отношений и полей ForeignKey, полей ManyToMany и т. Д. c

  • field.auto_created - будет True для полей, автоматически созданных с помощью django - обратные отношения, pk / id AutoField (но у него будет is_relation == False)

  • field.many_to_many - будет True для полей ManyToMany и отношений

Таким образом, вы можете выбрать обязательные поля или отношения, вперед или назад, много ко многим или нет. И точно зная тип отношения - создавайте объекты соответствующим образом, то есть добавляя к множеству для ManyToMany.

Поле отношения Значение (связанный объект или объекты) можно получить с помощью доступа getattr, _meta или с помощью запроса:

children = field.related_model.objects.filter(
    **{field.remote_field.name: object}
)

Действительно для отношений и полей с отношениями.


Примечания:

  • , поскольку это, вероятно, будет очень конкретным приложением c как далеко Вы хотите клонировать отношения вверх, вниз и вбок (включая родителей и их родителей; детей детей; отношения с моделью fk на или отношения с моделью fk до ; следуйте инструкциям fks на других моделях для детей / родителей) или отфильтруйте, какие модели разрешены и т. д. - возможно, будет нормально иметь метод clone, более привязанный к спецификациям c структура модели

  • также имеются скрытые отношения - те, которые определены с related_name = "+" для ForeignKey или ManyToManyField. Их все еще можно обнаружить с помощью параметра include_hidden : object._meta.get_fields(include_hidden=True)

0 голосов
/ 11 мая 2020

Было бы легче понять, чем помочь, если бы вы могли указать версию Django, которую вы используете, и что на самом деле вы пытаетесь достичь путем клонирования. Django связанные поля работают по-разному в разных версиях, так как прямая и обратная ссылки имеют разные способы работы. Так что, если бы вы могли сказать, что именно вы пытаетесь сделать со своим кодом?

0 голосов
/ 06 мая 2020

Поскольку у вас есть отношение M2M, вам нужно будет создать новые записи в связанной таблице. Для этого add () кажется более подходящим. Вы можете попробовать что-то вроде этого:

for old_child in relation.related_model.objects.all():
    new_child = self.clone_object(old_child)
    setattr(new_child, remote_field_name, object)
    relation.related_model.objects.add(new_child)

Обратите внимание, что этот код не проверен , поэтому может потребоваться некоторая корректировка.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...