You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Django-Simple-History:避免冗余历史记录并跟踪通用关系

Django-Simple-History:避免冗余历史记录并跟踪通用关系

问题描述

我们正在使用django-simple-history来跟踪模型中的变更,包括多对多(m2m)关系和通用关系。

Model 代码

class BandProfile(BaseModel):

    class Meta:
        db_table = "band_profiles"

    name = models.CharField(max_length=255)
    types = models.ManyToManyField(
        Choices,
        related_name="types",
        limit_choices_to={"category": "type"},
        blank=True,
    )
    description = models.TextField(null=True, blank=True)

    history = HistoricalRecords(
        table_name="history_band_profiles",
        history_user_setter=set_historical_user_from_request,
        history_user_getter=get_historical_user_from_request,
        history_user_id_field=models.CharField(max_length=36, null=True),
        m2m_fields=[types],  # Many-to-many tracking
    )

    def __str__(self):
        return f"Band Profile for {self.name}"

Serializer 代码(create方法)

def create(self, validated_data):
    """
    Create method for BandProfile with handling for related fields.
    """
    types = validated_data.pop("type_ids", [])

    band_profile = BandProfile.objects.create(**validated_data)
    band_profile.types.set(types)

遇到的问题

在创建或更新资源时,django-simple-history会生成多条冗余的历史记录,导致历史跟踪信息不正确。

期望的响应格式

我们需要结构化的响应,包含资源的历史记录、其多对多关系和通用关系的历史,示例如下:

[
    {
        "id": 101,
        "name": "Resource A",
        "history_date": "2025-03-20T10:00:00Z",
        "change_type": "update",
        "types": [
            {
                "id": 201,
                "name": "Sector X",
                "history_date": "2025-03-19T15:00:00Z",
                "change_type": "create"
            },
            {
                "id": 202,
                "name": "Sector Y",
                "history_date": "2025-03-20T16:00:00Z",
                "change_type": "update"
            }
        ],
        "generic_relation": [
            {
                "id": 301,
                "type": "Document",
                "title": "Policy Document A",
                "history_date": "2025-03-18T11:00:00Z",
                "change_type": "create"
            },
            {
                "id": 302,
                "type": "Document",
                "title": "Policy Document B",
                "history_date": "2025-03-20T14:00:00Z",
                "change_type": "delete"
            }
        ]
    }
]

我们的疑问

如何在创建和更新多对多关系时避免冗余的历史记录?我们的配置是否有遗漏?

备注:我们尝试过多对多关系使用through模型,但历史记录无法维护主模型和关联对象之间的链接。


解决方案

咱们先拆解问题:冗余记录的根源和通用关系跟踪的实现,分别给你实用的解决思路:

一、解决冗余历史记录的问题

你现在遇到的多条记录,核心原因是:创建主对象和设置m2m字段是两个独立的操作,django-simple-history会分别为每个操作生成历史条目。比如你先create主对象(触发第一条记录),再调用types.set()(触发第二条记录,因为你配置了m2m_fields)。下面是具体的修复方法:

1. 用原子事务+临时禁用自动记录,手动生成统一历史条目

把主对象创建和m2m关联的操作放在同一个原子事务里,临时禁用自动历史记录,完成所有操作后手动生成一条包含所有初始状态的历史记录。这样就能确保只生成一条记录,包含主对象和m2m的完整初始状态。

修改你的Serializer的create方法:

from django.db import transaction
from simple_history.utils import disable_history

def create(self, validated_data):
    """
    Create method for BandProfile with single history entry for all changes.
    """
    type_ids = validated_data.pop("type_ids", [])
    request = self.context.get("request")

    with transaction.atomic(), disable_history(BandProfile):
        # 在禁用历史的上下文里执行所有操作,不会触发自动记录
        band_profile = BandProfile.objects.create(**validated_data)
        band_profile.types.set(type_ids)
    
    # 手动创建一条包含完整状态的历史记录
    historical_entry = band_profile.history.create(
        history_type="+",  # "+"表示创建操作,更新用"~",删除用"-"
        history_user_id=get_historical_user_from_request(request) if request else None,
        # 其他历史字段会自动从当前对象同步
    )
    # 如果你的自定义setter需要额外处理,这里可以调用set_historical_user_from_request
    set_historical_user_from_request(historical_entry, request)
    
    return band_profile

同样,更新操作也可以用这个思路:把主对象更新和m2m变更放在disable_history上下文里,之后手动生成一条更新的历史记录。

2. 检查自定义历史用户配置是否重复触发

你的HistoricalRecords配置里用了自定义的history_user_settergetter,要确保这些函数不会在m2m变更时重复执行。比如,当调用types.set()时,是否会再次触发set_historical_user_from_request,导致生成额外的记录?可以在这些函数里加日志排查,或者确保它们只在主对象的save()操作时执行。

3. 关于through模型的正确用法(可选)

如果你一定要用through模型,需要给through模型也添加HistoricalRecords,并且关联到主模型的历史记录。比如:

class BandProfileTypeThrough(models.Model):
    band_profile = models.ForeignKey(BandProfile, on_delete=models.CASCADE)
    type = models.ForeignKey(Choices, on_delete=models.CASCADE)
    
    history = HistoricalRecords(table_name="history_band_profile_types")

# 然后在BandProfile的m2m配置里指定through:
types = models.ManyToManyField(
    Choices,
    related_name="types",
    limit_choices_to={"category": "type"},
    blank=True,
    through=BandProfileTypeThrough,
)

不过这种方式会生成through模型的历史记录,需要额外处理主模型和through模型的历史关联,不如第一种方法直接。

二、实现通用关系的历史跟踪

django-simple-history默认不支持直接跟踪GenericForeignKey,需要我们手动处理,核心思路是:给通用关系指向的所有模型添加历史记录,然后在序列化主模型的历史数据时,嵌套查询对应的通用对象历史。

1. 给通用模型添加历史记录

比如,如果你的通用关系指向Document模型,给它加上HistoricalRecords

class Document(BaseModel):
    title = models.CharField(max_length=255)
    # 其他字段...
    history = HistoricalRecords(table_name="history_documents")

2. 自定义历史序列化器,嵌套通用关系的历史

创建一个主模型的历史序列化器,在里面手动查询对应时间点的通用对象历史,嵌套返回,符合你期望的格式:

from simple_history.models import HistoricalRecords

class HistoricalBandProfileSerializer(serializers.ModelSerializer):
    types = serializers.SerializerMethodField()
    generic_relation = serializers.SerializerMethodField()

    class Meta:
        model = BandProfile.history.model
        fields = ["id", "name", "history_date", "change_type", "types", "generic_relation"]
    
    def get_types(self, historical_obj):
        # 获取该历史记录对应的types关联的当前历史状态
        # 这里假设Choices模型也有HistoricalRecords
        return HistoricalChoicesSerializer(
            historical_obj.instance.types.all(), many=True
        ).data
    
    def get_generic_relation(self, historical_obj):
        # 获取关联的通用对象在该历史时间点的状态
        generic_obj = historical_obj.instance.generic_obj  # 你的通用关系字段名
        if not generic_obj:
            return []
        
        # 获取该通用对象在历史记录生成时间点的快照
        generic_history = generic_obj.history.as_of(historical_obj.history_date)
        # 用对应的历史序列化器返回数据
        return HistoricalDocumentSerializer(generic_history).data

这样,当你返回主模型的历史数据时,就能看到嵌套的m2m和通用关系的历史记录了。

三、额外的注意事项

  • 所有操作尽量放在原子事务里,避免部分操作失败导致历史记录不一致
  • 手动生成历史记录时,要确保history_user和其他自定义字段的设置和自动生成的逻辑一致
  • 测试更新操作时,同样要把主对象字段更新和m2m变更放在同一个disable_history上下文里,避免多条记录

备注:内容来源于stack exchange,提问作者user30040602

火山引擎 最新活动