Django, cascading move to a separate table instead of cascading delete
我想在我们结束时保留数据。
我想将数据移动到另一个表(对于已删除的行),而不是
https://stackoverflow.com/a/26125927/433570
我也不知道战略的名称是什么。称为存档?删除两个表?
为了使这项工作顺利进行,
我需要能够做到
对于给定的对象(将被删除),查找具有该对象的外键或一对一键的所有其他对象。(这可以通过https://stackoverflow.com/a/2315053/433570来实现,实际上比这更难,因为代码还不够)
插入一个新对象,并让在1中找到的所有对象指向这个新对象
删除对象
(重要的是,我要做层叠移动,而不是层叠删除,1~3步应该以递归的方式完成)
对于一个对象和一个查询集,为了支持
有人创造过这样的吗?
我自己实现了这一点,我正在分享我的发现。
档案文件第一次归档相当容易,因为我放宽了对归档表的foreignkey约束。
您不能像在活动世界中那样保留存档世界中的所有约束,因为您要删除的对象所引用的内容将不在存档世界中。(因为它不会被删除)
这可以通过mixin(系统地)完成。
基本上,使用级联创建归档对象,然后删除原始对象。
未归档另一方面,由于您需要确认外键约束,因此无存档更为困难。这不能系统地进行。
这也是DjangoRest框架等序列化程序无法神奇地创建相关对象的原因。你必须知道对象图和约束。
所以这就是为什么没有图书馆或者混音器来支持这一点。
不管怎样,我在下面共享我的mixin代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | 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 |
如果您正在为特定的服务或功能寻找任何第三方Django软件包,如果您不了解现有的软件包,您可以随时在www.djangopackages.com上搜索。它还将为您提供包之间的比较表,以帮助您做出正确的选择。从表中可以看出:Django Reversion是最常用的版本,有稳定的版本,Github中活跃的社区,最近一次更新是3天前,这意味着项目维护得很好,很可靠。
要安装Django Reversion,请执行以下步骤:
1.采用PIP:
2.在
3.运行
查看此处了解更多详细信息和配置