当我们删除
而不是软时,我想保留数据 - 删除(使用is_deleted字段),我想将数据移动到另一个表(删除的行)
instead of soft-delete (which uses is_deleted field), I'd like to move the data to another table (for deleted rows)
stackoverflow/a/26125927/433570
我不知道战略的名称是什么。叫归档?两张表删除?
I don't know what is the name of the strategy either. called archiving? two-table delete?
为了使这项工作,
我需要能够做
I need to be able to do
,找到具有外键或一对一键的所有其他对象目的。 (这可以通过 stackoverflow/a/2315053/433570 完成,实际上比这更难代码不够)
for a given object(which will be deleted), find all other objects that has foreign key or one-to-one key to the object. (this can be done via stackoverflow/a/2315053/433570, actually harder than that, that code isn't sufficient)
插入一个新对象,并将#1中找到的所有对象指向这个新对象
insert a new object and have all the objects found in #1 to point to this new object
删除对象
(essentiall我正在进行级联移动级联删除,1〜3步应该以递归的方式进行)
(essentiall I'm doing cascading move instead of cascading delete, 1~3 step should be done in recursive fashion)
最为方便的是为此支持 delete ()和 undelete()为一个对象和一个查询器。
It would be most convenient to make a mixin for this that supports delete() and undelete() for a object and for a queryset.
任何人创建了这样的一个?
Has anyone created one like this?
推荐答案我自己实现了这一点,我正在分享我的发现。
I implemented this myself and I'm sharing my findings.
首先归档很简单,因为我放宽了归档表上的外键限制。
First archiving is rather easy since I relaxed foreignkey constraints on archive tables.
你不能在arch中保留所有约束ive世界就像在现实世界中一样,因为你所要删除的对象所引用的内容不会在档案世界中。 (因为它不会被删除)
You can't keep all constraints in the archive world as you would have in the live world, because what your to-be-deleted object is refering to won't be in the archive world. (because it won't be deleted)
这可以通过mixin(系统地)完成
This can be done via mixin (systematically)
您将使用级联创建归档对象,然后删除原始文件。
Basically, you create archive objects with cascade then delete the original.
另一方面,取消归档更难,因为您需要确认外键约束。 这不能系统地完成。
On the other hand, unarchiving is harder, because you need to confirm to the foreign key constraints. This can't be done systematically.
Django休息框架之类的序列化程序不会神奇地创建相关对象,这也是一样的原因。你必须知道对象图和约束。
It's the same reason why serializers such as Django rest framework won't create related objects magically. You have to know the object graph and constraints.
所以这就是为什么没有图书馆或者混合在这里来支持这个。
So that's why there's no library or mixin out there to support this.
无论如何,我在下面分享我的mixin代码。
Anyway, I'm sharing my mixin code below.
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): # stackoverflow/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): # stackoverflow/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更多推荐
Django,级联移动到单独的表,而不是级联删除
发布评论