Django, каскадно преместване в отделна таблица вместо каскадно изтриване

Бих искал да запазя данните, когато delete

вместо soft-delete (което използва полето is_deleted), бих искал да преместя данните в друга таблица (за изтрити редове)

https://stackoverflow.com/a/26125927/433570

И аз не знам как се казва стратегията. наречено архивиране? изтриване на две таблици?

За да работи това,

Трябва да мога да направя

  1. за даден обект (който ще бъде изтрит), намерете всички други обекти, които имат външен ключ или ключ едно към едно към обекта. (това може да се направи чрез https://stackoverflow.com/a/2315053/433570, всъщност по-трудно от това, че кодът не е достатъчен)

  2. вмъкнете нов обект и всички обекти, намерени в #1, да сочат към този нов обект

  3. изтрийте обекта

(по същество правя каскадно преместване вместо каскадно изтриване, 1~3 стъпка трябва да се направи по рекурсивен начин)

Би било най-удобно да се направи миксин за това, който поддържа delete() и undelete() за обект и за набор от заявки.

Някой създавал ли е такъв?


person eugene    schedule 20.12.2015    source източник
comment
Редактирах въпроса, така че как да накарам това да излезе от състояние put-on-hold?   -  person eugene    schedule 21.12.2015
comment
здравейте, моля, отворете отново това :(   -  person eugene    schedule 21.12.2015


Отговори (2)


Приложих това сам и споделям констатациите си.

Архив

Първото архивиране е доста лесно, тъй като облекчих ограниченията на чуждия ключ върху архивните таблици.

Не можете да запазите всички ограничения в света на архивите, както бихте имали в света на живо, защото това, към което се отнася вашият обект, който трябва да бъде изтрит, няма да бъде в света на архивите. (защото няма да се изтрие)

Това може да стане чрез mixin (систематично)

По принцип създавате архивни обекти с каскада, след което изтривате оригинала.

Разархивиране

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

Това е същата причина, поради която сериализаторите като Django rest framework няма да създават магически свързани обекти. Трябва да знаете графиката на обекта и ограниченията.

Ето защо няма библиотека или миксин, които да поддържат това.

Както и да е, споделям моя миксин код по-долу.

 class DeleteModelQuerySet(object):
     '''
     take a look at django.db.models.deletion
     '''

     def hard_delete(self):
         super().delete()

     def delete(self):
         if not self.is_archivable():
             super().delete()
             return

         archive_object_ids = []
         seen = []

         collector = NestedObjects(using='default')  # or specific database
         collector.collect(list(self))
         collector.sort()

         with transaction.atomic():

             for model, instances in six.iteritems(collector.data):

                 if model in self.model.exclude_models_from_archive():
                     continue

                 assert hasattr(model, "is_archivable"), {
                     "model {} doesn't know about archive".format(model)
                 }

                 if not model.is_archivable():
                     # just delete
                     continue

                 for instance in instances:

                     if instance in seen:
                         continue
                     seen.append(instance)

                     for ptr in six.itervalues(instance._meta.parents):
                         # add parents to seen
                         if ptr:
                             seen.append(getattr(instance, ptr.name))

                     archive_object = model.create_archive_object(instance)
                     archive_object_ids.append(archive_object.id)

             # real delete
             super().delete()

         archive_objects = self.model.get_archive_model().objects.filter(id__in=archive_object_ids)
         return archive_objects

     def undelete(self):

         with transaction.atomic():
             self.unarchive()

             super().delete()

     def is_archivable(self):
         # if false, we hard delete instead of archive
         return self.model.is_archivable()

     def unarchive(self):

         for obj_archive in self:
             self.model.create_live_object(obj_archive)


 class DeleteModelMixin(models.Model):

     @classmethod
     def is_archivable(cls):
         # override if you don't want to archive and just delete
         return True

     def get_deletable_objects(self):
         collector = NestedObjects(using='default')  # or specific database
         collector.collect(list(self))
         collector.sort()
         deletable_data = collector.data

         return deletable_data

     @classmethod
     def create_archive_object(cls, obj):
         # http://stackoverflow.com/q/21925671/433570
         # d = cls.objects.filter(id=obj.id).values()[0]

         d = obj.__dict__.copy()
         remove_fields = []
         for field_name, value in six.iteritems(d):
             try:
                 obj._meta.get_field(field_name)
             except FieldDoesNotExist:
                 remove_fields.append(field_name)
         for remove_field in remove_fields:
             d.pop(remove_field)

         cls.convert_to_archive_dictionary(d)

         # print(d)

         archive_object = cls.get_archive_model().objects.create(**d)
         return archive_object

     @classmethod
     def create_live_object(cls, obj):

         # index error, dont know why..
         # d = cls.objects.filter(id=obj.id).values()[0]

         d = obj.__dict__.copy()

         remove_fields = [cls.convert_to_archive_field_name(field_name) + '_id' for field_name in cls.get_twostep_field_names()]
         for field_name, value in six.iteritems(d):
             try:
                 obj._meta.get_field(field_name)
             except FieldDoesNotExist:
                 remove_fields.append(field_name)

         for remove_field in remove_fields:
             d.pop(remove_field)

         cls.convert_to_live_dictionary(d)

         live_object = cls.get_live_model().objects.create(**d)
         return live_object

     @classmethod
     def get_archive_model_name(cls):
         return '{}Archive'.format(cls._meta.model_name)

     @classmethod
     def get_live_model_name(cls):

         if cls._meta.model_name.endswith("archive"):
             length = len("Archive")
             return cls._meta.model_name[:-length]
         return cls._meta.model_name

     @classmethod
     def get_archive_model(cls):
         # http://stackoverflow.com/a/26126935/433570
         return apps.get_model(app_label=cls._meta.app_label, model_name=cls.get_archive_model_name())

     @classmethod
     def get_live_model(cls):
         return apps.get_model(app_label=cls._meta.app_label, model_name=cls.get_live_model_name())

     @classmethod
     def is_archive_model(cls):
         if cls._meta.model_name.endswith("Archive"):
             return True
         return False

     @classmethod
     def is_live_model(cls):
         if cls.is_archive_model():
             return False
         return True

     def make_referers_point_to_archive(self, archive_object, seen):

         instance = self

         for related in get_candidate_relations_to_delete(instance._meta):
             accessor_name = related.get_accessor_name()

             if accessor_name.endswith('+') or accessor_name.lower().endswith("archive"):
                 continue

             referers = None

             if related.one_to_one:
                 referer = getattr(instance, accessor_name, None)
                 if referer:
                     referers = type(referer).objects.filter(id=referer.id)
             else:
                 referers = getattr(instance, accessor_name).all()

             refering_field_name = '{}_archive'.format(related.field.name)

             if referers:
                 assert hasattr(referers, 'is_archivable'), {
                     "referers is not archivable: {referer_cls}".format(
                         referer_cls=referers.model
                     )
                 }

                 archive_referers = referers.delete(seen=seen)
                 if referers.is_archivable():
                     archive_referers.update(**{refering_field_name: archive_object})

     def hard_delete(self):
         super().delete()

     def delete(self, *args, **kwargs):
         self._meta.model.objects.filter(id=self.id).delete()

     def undelete(self, commit=True):
         self._meta.model.objects.filter(id=self.id).undelete()

     def unarchive(self, commit=True):
         self._meta.model.objects.filter(id=self.id).unarchive()

     @classmethod
     def get_archive_field_names(cls):
         raise NotImplementedError('get_archive_field_names() must be implemented')

     @classmethod
     def convert_to_archive_dictionary(cls, d):

         field_names = cls.get_archive_field_names()
         for field_name in field_names:
             field_name = '{}_id'.format(field_name)
             archive_field_name = cls.convert_to_archive_field_name(field_name)
             d[archive_field_name] = d.pop(field_name)

     @classmethod
     def convert_to_live_dictionary(cls, d):

         field_names = list(set(cls.get_archive_field_names()) - set(cls.get_twostep_field_names()))

         for field_name in field_names:
             field_name = '{}_id'.format(field_name)
             archive_field_name = cls.convert_to_archive_field_name(field_name)
             d[field_name] = d.pop(archive_field_name)

     @classmethod
     def convert_to_archive_field_name(cls, field_name):
         if field_name.endswith('_id'):
             length = len('_id')
             return '{}_archive_id'.format(field_name[:-length])
         return '{}_archive'.format(field_name)

     @classmethod
     def convert_to_live_field_name(cls, field_name):
         if field_name.endswith('_archive_id'):
             length = len('_archive_id')
             return '{}_id'.format(field_name[:-length])
         if field_name.endswith('archive'):
             length = len('_archive')
             return '{}'.format(field_name[:-length])
         return None

     @classmethod
     def get_twostep_field_names(cls):
         return []

     @classmethod
     def exclude_models_from_archive(cls):
         # excluded model can be deleted if referencing to me
         # or just lives if I reference him
         return []

     class Meta:
         abstract = True
person eugene    schedule 24.12.2015
comment
Направих Gist на този код и добавих липсващите импортирания: gist.github.com/adamlwgriffiths/4fb6a47bb84ec6e34424 - person Rebs; 06.02.2016

Ако търсите django-пакет на трета страна за конкретна услуга или функционалност, винаги можете да търсите в www.djangopackages.com ако нямате представа за съществуващия. Той ще ви предостави и сравнителна таблица между пакетите, за да ви помогне да направите правилния избор. Въз основа на таблицата тук: django-reversion е най-използвани, имат стабилна версия, активна общност в github и последната актуализация е преди 3 дни, което означава, че проектът е много добре поддържан и надежден.

За да инсталирате django-reversion, изпълнете следните стъпки:

1. Инсталирайте с pip: pip install django-reversion.

2. Добавете „връщане“ към INSTALLED_APPS.

3.Изпълни manage.py migrate

Проверете тук за повече подробности и конфигурация

person DhiaTN    schedule 20.12.2015
comment
Имах предвид soft-delete да означава методология за използване на is_delete. Редактирах оп. Извинете за объркването. - person eugene; 21.12.2015
comment
Разгледах django-reversion, не мисля, че поддържа delete-undelete - person eugene; 23.12.2015
comment
Изглежда, че всички те използват поле deleted_on, вместо да преместват модели в друга таблица. - person Rebs; 06.02.2016