实体框架Code First和SQL Server 2012 Sequences

3

我正在实施数据库审计跟踪,通过我的Web API项目中的控制器执行的CRUD操作将序列化旧的和新的poco,并存储它们的值以供以后检索(历史记录,回滚等)。

当我把所有东西都弄好之后,我不喜欢它在POST期间使我的控制器看起来如此。这是因为我最终不得不调用SaveChanges()两次,一次是为了获取已插入实体的ID,然后再提交需要知道该ID的审计记录。

我开始将该项目(仍处于初级阶段)转换为使用序列而不是标识列。这具有进一步使我与SQL Server进行抽象化的附加优点,尽管这并不是真正的问题,但它还允许我减少提交次数,并让我将那个逻辑从控制器中提取出来并将其放入我的服务层中,从而使我的控制器从存储库进行抽象化,并允许我在该“垫片”层中执行此类审计工作。

一旦创建了Sequence对象和公开它的存储过程,我创建了以下类:

public class SequentialIdProvider : ISequentialIdProvider
{
    private readonly IService<SequenceValue> _sequenceValueService;

    public SequentialIdProvider(IService<SequenceValue> sequenceValueService)
    {
        _sequenceValueService = sequenceValueService;
    }

    public int GetNextId()
    {
        var value = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = 1 }).ToList();
        if (value.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        return value.First().FirstValue;
    }

    public IList<int> GetNextIds(int numberOfIds)
    {
        var values = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = numberOfIds }).ToList();
        if (values.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        var list = new List<int>();
        for (var i = values.First().FirstValue; i <= values.First().LastValue; i++)
        {
            list.Add(i);
        }

        return list;
    }
}

该方法提供了两种获取ID的方式,分别是单个ID和一组ID。

这在第一批单元测试中表现得非常出色,但当我开始在真实环境中进行测试时,很快就意识到对 GetNextId() 的单次调用将为该上下文的生命周期返回相同的值,直到调用 SaveChanges(),因此没有任何实际益处。

除了创建第二个上下文(不可选项)或使用老式 ADO.NET 进行直接 SQL 调用并使用 AutoMapper 来获得相同的净结果之外,我不确定是否有其他方法解决这个问题。这两种方法都不吸引我,所以我希望有人能给我一个想法。


你的审计记录是什么样子的?每个表一个,还是所有表共用一个?审计中存储了哪些信息?你审计哪些操作?你是否有通用数据层? - JotaBe
包含以下数据的单个审计表:旧数据或新数据的序列化POCO(在PUT的情况下可能同时包含两者),受影响的表,该表中的记录ID,更改列的列表以及生成审计的操作(插入、删除、更新)。 - James Legan
那么就没有其他方法了。两个SaveChanges选项似乎是唯一的解决方案。你不应该过于担心这个问题,有两个原因:1).NET使用连接池,2)EF不会批量发送查询,而是逐个发送。因此,使用两个SaveChanges或不使用的差异应该可以忽略不计。唯一的其他选择是将GUID用作PK:https://dev59.com/questions/YWMl5IYBdhLWcg3wsIuA但这会增加更多的缺点(索引性能差),因为GUID太长了。 - JotaBe
@JotaBe - 我并不太担心性能问题,而是关注我必须在哪里执行工作以将其全部联系起来。我的UoW和Repository彼此独立。我的Repository没有引用我的UoW,只有我的控制器有。因此,如果我采用双SaveChanges()方法,我必须在我的控制器中执行工作,这会增加很多噪音,我宁愿将其放在抽象Repository的服务类中。我使用容器构建了一个解决方案,将连接字符串注入到ADONet类中,并暂时采用这种方法。 - James Legan
1个回答

0
不知道这是否对你有所帮助,但这是我如何使用Code First进行审计日志跟踪的方法。以下代码编写在一个继承自DbContext的类中。
在我的构造函数中,我有以下内容:
IObjectContextAdapter objectContextAdapter = (this as IObjectContextAdapter);
objectContextAdapter.ObjectContext.SavingChanges += SavingChanges;

这是我之前连接的保存更改方法

void SavingChanges(object sender, EventArgs e) {
        Debug.Assert(sender != null, "Sender can't be null");
        Debug.Assert(sender is ObjectContext, "Sender not instance of ObjectContext");

        ObjectContext context = (sender as ObjectContext);
        IEnumerable<ObjectStateEntry> modifiedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
        IEnumerable<ObjectStateEntry> addedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added);

        addedEntities.ToList().ForEach(a => {
            //Assign ids to objects that don't have
            if (a.Entity is IIdentity && (a.Entity as IIdentity).Id == Guid.Empty)
                (a.Entity as IIdentity).Id = Guid.NewGuid();

            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(a, _AddedEntry));
        });

        modifiedEntities.ToList().ForEach(m => {
            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(m, _ModifiedEntry));
        });
    }

以下是先前用于构建审核日志详细信息的方法

private AuditLogEntry AuditLogEntryFactory(ObjectStateEntry entry, string entryType) {
        AuditLogEntry auditLogEntry = new AuditLogEntry() {
            EntryDate = DateTime.Now,
            EntryType = entryType,
            Id = Guid.NewGuid(),
            NewValues = AuditLogEntryNewValues(entry),
            Table = entry.EntitySet.Name,
            UserId = _UserId
        };

        if (entryType == _ModifiedEntry) auditLogEntry.OriginalValues = AuditLogEntryOriginalValues(entry);

        return auditLogEntry;
    }

    /// <summary>
    /// Creates a string of all modified properties for an entity.
    /// </summary>
    private string AuditLogEntryOriginalValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        entry.GetModifiedProperties().ToList().ForEach(m => {
            stringBuilder.Append(String.Format("{0} = {1},", m, entry.OriginalValues[m]));
        });

        return stringBuilder.ToString();
    }

    /// <summary>
    /// Creates a string of all modified properties' new values for an entity.
    /// </summary>
    private string AuditLogEntryNewValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < entry.CurrentValues.FieldCount; i++) {
            stringBuilder.Append(String.Format("{0} = {1},",
                entry.CurrentValues.GetName(i), entry.CurrentValues.GetValue(i)));
        }

        return stringBuilder.ToString();
    }

希望这能指引您朝着解决问题的方向前进。

通过这种方法(我已经将其全部连接并工作),我发现在一种分离的、代码优先的存储库模式中,“CurrentValues” 和 “OriginalValues” 是相同的。例如,在我的 RESTful Web API 中,当更新到来时,我会查找正在更新的 ID(它是分离的),进行必要的更改,然后将更新传递到存储库。当 EF 附加实体时,原始值和当前值都是相同的。这是框架的一个已知限制(当时不是我所知道的)。您可以在此处查看功能投票 http://entityframework.codeplex.com/workitem/864 - James Legan
谢谢您的评论@JDBuckSavage,这是我不知道的事情,需要去检查我的单元测试以确保我的应用程序功能正确。 - 3dd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接