我想保留数据 delete
而不是soft-delete
(使用is_deleted字段),我想将数据移动到另一个表(对于已删除的行)
/sf/ask/17360801/
我也不知道这个策略的名称是什么.叫档案?两表删除?
为了使这项工作,
我需要能够做到
对于给定对象(将被删除),查找具有该对象的外键或一对一键的所有其他对象.(这可以通过/sf/ask/17360801/完成,实际上比这更难,代码不够)
插入一个新对象并让#1中找到的所有对象指向这个新对象
删除对象
(essentiall我正在做级联移动而不是级联删除,1~3步应该以递归方式完成)
这将是最方便的,以弥补这方面,它支持一个mixin delete()
和undelete()
一个对象和一个查询集.
有人创造过这样的吗?
我自己实现了这一点,并且我正在分享我的发现.
首先归档是相当容易的,因为我放宽了对归档表的外键约束.
您不能像在实际世界中那样保留存档世界中的所有约束,因为您要删除的对象所引用的内容将不在存档世界中.(因为它不会被删除)
这可以通过mixin(系统地)完成
基本上,您使用级联创建归档对象,然后删除原始对象.
另一方面,unarchiving更难,因为您需要确认外键约束.
这不能系统地完成.
这就是为什么像Django rest框架这样的序列化程序不会神奇地创建相关对象的原因.您必须知道对象图和约束.
所以这就是为什么没有库或mixin支持这一点.
无论如何,我在下面分享我的mixin代码.
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