Django 数据的版本控制

Django | django-reversion | 版本控制 | Model | 历史记录 | 回滚 | 删除 | 恢复

Posted by luoruiqing on July 30, 2020

在工作中经常会有需求 保留操作历史, 有时还需要能够回滚到某个版本, 甚至删除了也能恢复, django-reversion 能快速实现版本控制.

django-reversion

django-reversion is an extension to the Django web framework that provides version control for model instances.

安装过程

  • 安装模块 pip install django-reversion.
  • 添加 'reversion'settings.py 配置文件中 INSTALLED_APPS 的选项内
  • 运行迁移 manage.py migrate.

MySQL需要使用 InnoDB 引擎

快速开始

模型注册

1
2
3
4
5
6
7
from django.db import models
import reversion

@reversion.register() # 注册Model
class YourModel(models.Model):

    pass

Admin注册

admin注册后, 所有的改动将自动生成历史, 并可以回滚

1
2
3
4
5
6
7
from django.contrib import admin
from reversion.admin import VersionAdmin

@admin.register(YourModel) # 注册表
class YourModelAdmin(VersionAdmin): # 修改继承VersionAdmin

    pass

初始化

1
python manage.py createinitialrevisions # 初始化历史版本

注意: 每当注册新的Model时, 运行一次createinitialrevisions

查看结果

打开已注册的Model, 此时出现删除恢复按钮, 通过admin删除的模型即可通过此处恢复

img

打开已注册的Model, 出现历史按钮, 通过admin修改的模型即可通过此处回滚

img

通过上述方式完成了django-Admin的恢复以及回滚, 正常操作Admin即可体验效果

基础操作

创建历史

reversion.create_revision

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 开始创建历史版本
with reversion.create_revision():

    obj = YourModel(name="obj v1") # 这里是你的Model, 必须是注册过的Model
    obj.save() # 这里创建了新的YourModel实例

    # Store some meta-information.
    reversion.set_user(request.user) # 可以设置这个版本是由哪个用户创建的
    reversion.set_comment("Created revision 1")  # 设置版本的标注
    # 在上下文管理退出时, 自动将obj的第一个版本保存

# 对同一个对象创建第二个版本
with reversion.create_revision():
    obj.name = "obj v2"
    obj.save() # 对象变更

    # Store some meta-information.
    reversion.set_user(request.user)
    reversion.set_comment("Created revision 2")

上面的代码中是对一个obj保存了两个版本的信息, with 两块

查看历史

1
2
3
4
5
6
7
8
9
10
11
from reversion.models import Version

# instance 只的是注册过的Model实例对象, 例如 obj = YourModel.objects.get(id=1) 中, obj即为实例
versions = Version.objects.get_for_object(instance)
assert len(versions) == 2

# Check the serialized data for the first version.
assert versions[1].field_dict["name"] == "obj v1"

# Check the serialized data for the second version.
assert versions[0].field_dict["name"] == "obj v2"

versions中存放着一个实例的多个历史版本, 最新的下标为0

回滚版本

1
2
3
4
5
6
7
8
9
10
11
12
13
# 回退到上上个版本
versions[1].revision.revert()

# 刷新到数据库
obj.refresh_from_db()
assert obj.name == "version 1"

# 回退到上一个版本
versions[0].revision.revert()

# Check the model instance has been reverted.
obj.refresh_from_db()
assert obj.name == "version 2"

删除恢复

1
2
3
4
5
6
7
8
9
pk = obj.pk
obj.delete()

# Revert the second revision.
versions[0].revision.revert()

# Check the model has been restored to the database.
obj = YourModel.objects.get(pk=obj.pk)
assert obj.name == "version 2"

源数据

1
2
3
4
5
6
7
8
9
# Check the revision metadata for the first revision.
assert versions[1].revision.comment == "Created revision 1"
assert versions[1].revision.user == request.user
assert isinstance(versions[1].revision.date_created, datetime.datetime)

# Check the revision metadata for the second revision.
assert versions[0].revision.comment == "Created revision 2"
assert versions[0].revision.user == request.user
assert isinstance(versions[0].revision.date_created, datetime.datetime)

高级用法

视图

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
import json  # 示例使用的JSON序列化包
from django.views import View  # Django视图或RDF视图均可
from reversion.views import RevisionMixin  # 导入视图混合
from django.forms.models import model_to_dict as django_mtd  # Django转数据类型的包


class MyView(RevisionMixin, View):
    ''' 你的视图 '''
    revision_manage_manually = False # 是否手动管理版本
    revision_using = None # 默认数据库 default

    def revision_request_creates_revision(self, request):
        ''' 所有方法均保留操作记录 '''
        return True

    def post(self, request):
        # >>> Step 1
        obj = YourModel(name="obj v1") # 这里是你的Model, 必须是注册过的Model
        obj.save() # 这里创建了新的YourModel实例
        # >>> Step 2
        obj.name = "obj v2"
        obj.save() # 对象变更
        # 查询记录
        obj_dict = django_mtd(obj)
        return HttpResponse(json.dumps(obj_dict), content_type="application/json")

    get = post

参数解释:

  • revision_manage_manually = False: 若为True YourModel.save() 将不会生成记录
  • revision_using = None(‘default’) : 指定使用那个数据库来写入记录(可用于读写分离)
  • revision_request_creates_revision(request) 方法 : 默认情况下忽略 GET / HEAD / OPTIONS 三个请求的历史记录写入, 覆盖该方法来决定如何生成记录

若不覆盖 revision_request_creates_revision 方法, 在 忽略的请求方式下 的历史记录 不会创建, 但数据变更会 正常生效

中间件

将每个请求都加入的历史记录中, 则可以直接启用RevisionMiddleware中间件配置, reversion.middleware.RevisionMiddleware 加入 settings.pyMIDDLEWARE 配置项中.

注意: 会自动启用数据库事务, 需要结合业务场景做性能方面的考虑

  • RevisionMiddleware.manage_manually = False : 若为True YourModel.save() 将不会生成记录
  • RevisionMiddleware.using = None : 指定使用那个数据库来写入记录(可用于读写分离)
  • RevisionMiddleware.request_creates_revision(request) 方法 : 覆盖该方法来决定如何生成记录
  • RevisionMiddleware.atomic = True : 是否使用事务包裹请求 transaction.atomic()

RevisionMiddleware.request_creates_revision 覆盖的样例:

1
2
3
4
5
6
7
8
9
from reversion.middleware import RevisionMiddleware

class BypassRevisionMiddleware(RevisionMiddleware):

    def request_creates_revision(self, request):
        # Bypass the revision according to some header
        silent = request.META.get("HTTP_X_NOREVISION", "false")
        return super().request_creates_revision(request) and \
            silent != "true"

差异对比

img

django-reversion-compare

django-reversion-compare库是基于django-reversio库定制的, 可以对模型修改的历史进行差异对比

安装

  • 安装模块 pip install django-reversion-compare.
  • 注册到 INSTALLED_APPS
  • Admin修改继承的Admin类.

注册: 注意前后顺序

1
2
3
4
5
6
7
INSTALLED_APPS = (
    'django...',
    ...
    'reversion', # 基础模块
    'reversion_compare', # django-reversion扩展的对比模块
    ...
)

修改继承

1
2
3
4
5
6
7
8
from django.contrib import admin
from reversion_compare.admin import CompareVersionAdmin

from my_app.models import YourModel

@admin.register(YourModel) # 你的表
class YourModelAdmin(CompareVersionAdmin): # 这里由 VersionAdmin 改为 CompareVersionAdmin 即可
    pass

reversion.admin.VersionAdmin 父类改为 reversion_compare.admin.CompareVersionAdmin 即可

其他高级功能请点击这里了解

注意事项

  • Queryset.update() 方法不会生成历史记录
  • 每当注册新的Model时, 运行一次./manage.py createinitialrevisions