You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将含reversion库的Django项目迁移至Gin:如何实现版本控制?

在Golang(Gin)中实现Django reversion风格的版本控制功能

我们正在将基于Python(Django)的项目迁移至Golang(Gin)框架,原项目使用reversion库实现数据版本控制,需要在Golang中复刻对应功能。原Django代码如下:

@reversion.register()
class DocumentRecommendationMedia(models.Model):
    document = models.ForeignKey("Document.Document", on_delete=models.CASCADE)
    file = models.FileField(upload_to=settings.DOCUMENT_RECOMMENDATION_PATH)
    file_name = models.CharField(max_length=100)

    class Meta:
        db_table = "document_recommendation_media"

@reversion.register(fields=("recommendation", "date", "media_files"))
class DocumentRecommendation(ArchivableModel):
    document = models.ForeignKey("Document.Document", on_delete=models.CASCADE)
    recommendation = models.TextField(null=True, blank=True)
    date = models.DateTimeField(default=timezone.now)
    media_files = models.ManyToManyField(DocumentRecommendationMedia, blank=True)

    class Meta:
        db_table = "document_recommendation"

实现思路

Django reversion的核心是在数据变更时自动保存旧版本快照,并支持后续版本恢复。在Golang中我们可以基于GORM(Gin生态最常用ORM)实现类似逻辑,分为以下几个步骤:

1. 设计版本表结构

针对需要版本控制的模型,创建对应的版本表,存储每次变更前的字段快照、版本号、变更时间等信息。

示例版本模型

import (
    "gorm.io/gorm"
    "time"
)

// DocumentRecommendationMedia 原业务模型
type DocumentRecommendationMedia struct {
    gorm.Model
    DocumentID uint   `gorm:"column:document_id"`
    File       string `gorm:"column:file"`
    FileName   string `gorm:"column:file_name;size:100"`
}

func (DocumentRecommendationMedia) TableName() string {
    return "document_recommendation_media"
}

// DocumentRecommendationMediaVersion 版本快照表
type DocumentRecommendationMediaVersion struct {
    gorm.Model
    OriginalID    uint      `gorm:"column:original_id"` // 关联原记录ID
    DocumentID    uint      `gorm:"column:document_id"`
    File          string    `gorm:"column:file"`
    FileName      string    `gorm:"column:file_name;size:100"`
    VersionNumber int       `gorm:"column:version_number"` // 自增版本号
    CreatedAt     time.Time `gorm:"column:created_at"`     // 版本生成时间
}

func (DocumentRecommendationMediaVersion) TableName() string {
    return "document_recommendation_media_version"
}

// DocumentRecommendation 原业务模型
type DocumentRecommendation struct {
    gorm.Model
    DocumentID     uint                              `gorm:"column:document_id"`
    Recommendation *string                           `gorm:"column:recommendation;type:text"`
    Date           time.Time                         `gorm:"column:date"`
    MediaFiles     []DocumentRecommendationMedia     `gorm:"many2many:document_recommendation_media_files;"`
}

func (DocumentRecommendation) TableName() string {
    return "document_recommendation"
}

// DocumentRecommendationVersion 版本快照表(仅保存指定字段)
type DocumentRecommendationVersion struct {
    gorm.Model
    OriginalID     uint      `gorm:"column:original_id"`
    Recommendation *string   `gorm:"column:recommendation;type:text"`
    Date           time.Time `gorm:"column:date"`
    MediaFileIDs   []uint    `gorm:"column:media_file_ids;type:json"` // 多对多关系快照
    VersionNumber  int       `gorm:"column:version_number"`
    CreatedAt      time.Time `gorm:"column:created_at"`
}

func (DocumentRecommendationVersion) TableName() string {
    return "document_recommendation_version"
}

2. 自动保存版本逻辑

利用GORM的BeforeUpdate、BeforeDelete钩子函数,在数据变更前自动将当前状态保存到版本表。

示例钩子实现

// DocumentRecommendationMedia 更新前保存版本
func (m *DocumentRecommendationMedia) BeforeUpdate(tx *gorm.DB) error {
    // 查询当前记录的最新状态
    var current DocumentRecommendationMedia
    if err := tx.First(&current, m.ID).Error; err != nil {
        return err
    }
    // 获取当前最大版本号
    var maxVersion int
    tx.Model(&DocumentRecommendationMediaVersion{}).Where("original_id = ?", m.ID).Select("MAX(version_number)").Scan(&maxVersion)
    // 创建版本快照
    version := DocumentRecommendationMediaVersion{
        OriginalID:    current.ID,
        DocumentID:    current.DocumentID,
        File:          current.File,
        FileName:      current.FileName,
        VersionNumber: maxVersion + 1,
    }
    return tx.Create(&version).Error
}

// DocumentRecommendation 更新前保存指定字段的版本
func (r *DocumentRecommendation) BeforeUpdate(tx *gorm.DB) error {
    var current DocumentRecommendation
    // 预加载多对多关联数据
    if err := tx.Preload("MediaFiles").First(&current, r.ID).Error; err != nil {
        return err
    }
    // 收集多对多关联ID
    var mediaIDs []uint
    for _, media := range current.MediaFiles {
        mediaIDs = append(mediaIDs, media.ID)
    }
    // 获取当前最大版本号
    var maxVersion int
    tx.Model(&DocumentRecommendationVersion{}).Where("original_id = ?", r.ID).Select("MAX(version_number)").Scan(&maxVersion)
    // 创建版本快照
    version := DocumentRecommendationVersion{
        OriginalID:     current.ID,
        Recommendation: current.Recommendation,
        Date:           current.Date,
        MediaFileIDs:   mediaIDs,
        VersionNumber:  maxVersion + 1,
    }
    return tx.Create(&version).Error
}

3. 版本恢复功能

实现从版本表中取出指定版本数据,覆盖原业务表记录的逻辑。

示例恢复函数

// RestoreDocumentRecommendationVersion 恢复指定版本的DocumentRecommendation
func RestoreDocumentRecommendationVersion(tx *gorm.DB, originalID uint, versionNumber int) error {
    // 查询目标版本快照
    var version DocumentRecommendationVersion
    if err := tx.Where("original_id = ? AND version_number = ?", originalID, versionNumber).First(&version).Error; err != nil {
        return err
    }
    // 更新原业务记录的指定字段
    if err := tx.Model(&DocumentRecommendation{}).Where("id = ?", originalID).Updates(map[string]interface{}{
        "recommendation": version.Recommendation,
        "date":           version.Date,
    }).Error; err != nil {
        return err
    }
    // 恢复多对多关联:先清空原关联,再重新绑定版本快照中的媒体文件
    tx.Model(&DocumentRecommendation{}).Where("id = ?", originalID).Association("MediaFiles").Clear()
    var medias []DocumentRecommendationMedia
    tx.Where("id IN ?", version.MediaFileIDs).Find(&medias)
    return tx.Model(&DocumentRecommendation{}).Where("id = ?", originalID).Association("MediaFiles").Append(medias)
}

4. 集成到Gin接口中

在Gin的更新接口中直接使用GORM操作即可触发钩子自动保存版本,无需额外手动调用:

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func UpdateDocumentRecommendation(c *gin.Context) {
    db := c.MustGet("db").(*gorm.DB)
    var req struct {
        ID             uint      `json:"id"`
        Recommendation *string   `json:"recommendation"`
        Date           time.Time `json:"date"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    rec := DocumentRecommendation{ID: req.ID}
    if err := db.Model(&rec).Updates(map[string]interface{}{
        "recommendation": req.Recommendation,
        "date":           req.Date,
    }).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
}

注意事项

  • 多对多关系的版本控制需要单独存储关联ID快照,恢复时需重新建立关联。
  • 如需记录操作人,可在版本表中添加OperatorID字段,在钩子中从Gin上下文获取当前登录用户ID。
  • 可封装通用版本控制工具函数,避免重复编写钩子逻辑。

内容的提问来源于stack exchange,提问作者Jenny F

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 05:15:59