将含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(¤t, 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(¤t, 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
相关产品推荐
相关产品推荐

