关于数据库:Django,级联移动到单独的表而不是级联删除

Django, cascading move to a separate table instead of cascading delete

我想在我们结束时保留数据。

我想将数据移动到另一个表(对于已删除的行),而不是soft-delete(使用的是"已删除字段")。

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

我也不知道战略的名称是什么。称为存档?删除两个表?

为了使这项工作顺利进行,

我需要能够做到

  • 对于给定的对象(将被删除),查找具有该对象的外键或一对一键的所有其他对象。(这可以通过https://stackoverflow.com/a/2315053/433570来实现,实际上比这更难,因为代码还不够)

  • 插入一个新对象,并让在1中找到的所有对象指向这个新对象

  • 删除对象

  • (重要的是,我要做层叠移动,而不是层叠删除,1~3步应该以递归的方式完成)

    对于一个对象和一个查询集,为了支持delete()undelete(),这样做最方便。

    有人创造过这样的吗?


    我自己实现了这一点,我正在分享我的发现。

    档案文件

    第一次归档相当容易,因为我放宽了对归档表的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:pip install django-reversion安装。

    2.在INSTALLED_APPS中增加"还原"。

    3.运行manage.py migrate

    查看此处了解更多详细信息和配置