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

在C#/SQL Server应用中如何保存PO及关联Item的对象版本历史?

嘿,针对你这个PO和关联明细项的版本历史保存需求,我给你整理了一套在C# + SQL Server环境下的可落地实现方案,分数据库设计和代码逻辑两部分来说明,刚好匹配你给出的V1到V2的场景:

一、数据库设计(快照式方案)

这种方案的核心是保存每个版本的完整快照,能直接还原任意版本下PO和所有Item的状态,非常适合你的业务场景。我们需要4张表:主表存最新数据,版本表存历史快照。

表结构说明:

  • POs:PO主表,只保留当前最新版本的数据
  • POVersions:PO历史版本表,每更新一次就插入一条完整快照
  • Items:Item主表,保留当前最新的PO明细
  • ItemVersions:Item历史版本表,和PO版本关联,保存对应版本下的Item状态

SQL创建脚本:

-- PO主表
CREATE TABLE POs (
    ID INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    DeliveryDate DATE NOT NULL,
    CurrentVersionNumber INT NOT NULL DEFAULT 1 -- 初始版本为1
);

-- PO版本表
CREATE TABLE POVersions (
    POVersionID INT PRIMARY KEY IDENTITY(1,1),
    POID INT NOT NULL FOREIGN KEY REFERENCES POs(ID),
    VersionNumber INT NOT NULL,
    Name NVARCHAR(100) NOT NULL,
    DeliveryDate DATE NOT NULL,
    CreatedDateTime DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    CreatedBy NVARCHAR(50) NOT NULL,
    -- 复合索引,加速版本查询
    INDEX IX_POVersions_POID_Version (POID, VersionNumber)
);

-- Item主表
CREATE TABLE Items (
    ID INT PRIMARY KEY IDENTITY(1,1),
    POID INT NOT NULL FOREIGN KEY REFERENCES POs(ID),
    Name NVARCHAR(100) NOT NULL,
    Cost DECIMAL(18,2) NOT NULL,
    CurrentVersionNumber INT NOT NULL DEFAULT 1
);

-- Item版本表
CREATE TABLE ItemVersions (
    ItemVersionID INT PRIMARY KEY IDENTITY(1,1),
    ItemID INT NULL FOREIGN KEY REFERENCES Items(ID), -- 新增的Item会有ID,删除的话可以留空(如果需要支持删除)
    POID INT NOT NULL FOREIGN KEY REFERENCES POs(ID),
    VersionNumber INT NOT NULL,
    Name NVARCHAR(100) NOT NULL,
    Cost DECIMAL(18,2) NOT NULL,
    CreatedDateTime DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    CreatedBy NVARCHAR(50) NOT NULL,
    -- 复合索引
    INDEX IX_ItemVersions_POID_Version (POID, VersionNumber)
);
二、C#代码实现逻辑

这里用Entity Framework Core来示例,核心逻辑是用事务保证PO和Item版本的一致性,每次保存新版本时:

  1. 生成新的版本号(当前版本+1)
  2. 保存PO的新版本快照
  3. 更新PO主表为最新数据
  4. 处理所有Item:更新现有Item的快照和主表,新增Item则插入主表+快照
  5. 提交事务(失败则回滚)

实体类定义:

public class PO
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime DeliveryDate { get; set; }
    public int CurrentVersionNumber { get; set; }
    public ICollection<Item> Items { get; set; } = new List<Item>();
}

public class POVersion
{
    public int POVersionID { get; set; }
    public int POID { get; set; }
    public int VersionNumber { get; set; }
    public string Name { get; set; }
    public DateTime DeliveryDate { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public string CreatedBy { get; set; }
}

public class Item
{
    public int ID { get; set; }
    public int POID { get; set; }
    public string Name { get; set; }
    public decimal Cost { get; set; }
    public int CurrentVersionNumber { get; set; }
    public PO PO { get; set; }
}

public class ItemVersion
{
    public int ItemVersionID { get; set; }
    public int? ItemID { get; set; }
    public int POID { get; set; }
    public int VersionNumber { get; set; }
    public string Name { get; set; }
    public decimal Cost { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public string CreatedBy { get; set; }
}

保存版本的核心方法:

public async Task SavePOWithVersionAsync(PO updatedPO, string currentUserName)
{
    using var transaction = await _dbContext.Database.BeginTransactionAsync();
    try
    {
        // 获取当前PO的最新状态(包含关联的Item)
        var existingPO = await _dbContext.POs
            .Include(p => p.Items)
            .FirstAsync(p => p.ID == updatedPO.ID);

        int newVersion = existingPO.CurrentVersionNumber + 1;

        // 1. 保存PO的新版本快照
        var poVersion = new POVersion
        {
            POID = updatedPO.ID,
            VersionNumber = newVersion,
            Name = updatedPO.Name,
            DeliveryDate = updatedPO.DeliveryDate,
            CreatedDateTime = DateTime.UtcNow,
            CreatedBy = currentUserName
        };
        _dbContext.POVersions.Add(poVersion);

        // 2. 更新PO主表为最新数据
        existingPO.Name = updatedPO.Name;
        existingPO.DeliveryDate = updatedPO.DeliveryDate;
        existingPO.CurrentVersionNumber = newVersion;

        // 3. 处理现有Item:更新快照和主表
        var existingItemIds = existingPO.Items.Select(i => i.ID).ToList();
        foreach (var updatedItem in updatedPO.Items.Where(i => existingItemIds.Contains(i.ID)))
        {
            var existingItem = existingPO.Items.First(i => i.ID == updatedItem.ID);
            
            // 插入Item版本快照
            var itemVersion = new ItemVersion
            {
                ItemID = updatedItem.ID,
                POID = updatedPO.ID,
                VersionNumber = newVersion,
                Name = updatedItem.Name,
                Cost = updatedItem.Cost,
                CreatedDateTime = DateTime.UtcNow,
                CreatedBy = currentUserName
            };
            _dbContext.ItemVersions.Add(itemVersion);

            // 更新Item主表
            existingItem.Name = updatedItem.Name;
            existingItem.Cost = updatedItem.Cost;
            existingItem.CurrentVersionNumber = newVersion;
        }

        // 4. 处理新增的Item(比如你的V2里的Item3)
        foreach (var newItem in updatedPO.Items.Where(i => !existingItemIds.Contains(i.ID)))
        {
            // 先插入主表获取ID
            _dbContext.Items.Add(newItem);
            await _dbContext.SaveChangesAsync();

            // 插入Item版本快照
            var itemVersion = new ItemVersion
            {
                ItemID = newItem.ID,
                POID = updatedPO.ID,
                VersionNumber = newVersion,
                Name = newItem.Name,
                Cost = newItem.Cost,
                CreatedDateTime = DateTime.UtcNow,
                CreatedBy = currentUserName
            };
            _dbContext.ItemVersions.Add(itemVersion);
        }

        // 提交所有变更
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch (Exception ex)
    {
        await transaction.RollbackAsync();
        // 这里可以加日志记录,比如_logger.LogError(ex, "保存PO版本失败");
        throw;
    }
}
三、版本回溯示例

如果要还原PO的Version 2状态,直接查询关联的版本表即可:

public async Task<(POVersion PoSnapshot, List<ItemVersion> ItemSnapshots)> GetPOVersionAsync(int poId, int versionNumber)
{
    var poSnapshot = await _dbContext.POVersions
        .FirstAsync(v => v.POID == poId && v.VersionNumber == versionNumber);

    var itemSnapshots = await _dbContext.ItemVersions
        .Where(v => v.POID == poId && v.VersionNumber == versionNumber)
        .ToListAsync();

    return (poSnapshot, itemSnapshots);
}
四、优化建议
  • 增量式优化:如果担心快照占用空间,可以只保存变化的字段(比如加ChangedFields字段记录修改的属性),但回溯逻辑会更复杂,适合数据量极大的场景;
  • 审计字段:可以在版本表中增加ChangeReason字段,让用户填写修改原因,方便后续审计;
  • 索引优化:给版本表的POID+VersionNumber加复合索引,大幅提升版本查询速度;
  • 软删除支持:如果需要支持删除Item,不要直接删除主表数据,而是在主表加IsDeleted字段,同时在版本表中保存删除状态的快照。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:04:39