管理 Entity Framework 上下文的最佳方法是什么?

What is the best way to manage Entity Framework contexts?

我有这样的东西:

static Employee getEmployee(string ssn){
    MyEntity context = null;
    Employee employee;
    try{
        context = new MyEntity();
        employee = context.Employee.Where(X => X.ssn.Equals(ssn));
    }
    catch (Exception exc){
        LogLibrary.WriteLog(exc.Message);
    }
    finally{
        if (context != null){
            context.Database.Connection.Close();
        }
    }
    return employee;
}

static Employee addEmployee(Employee emp){
    MyEntity context = null;
    Employee employee;
    try{
        context = new MyEntity();
        context.Employee.Add(e);
    }
    catch (Exception exc){
        LogLibrary.WriteLog(exc.Message);
    }
    finally{
        if (context != null){
            context.Database.Connection.Close();
        }
    }
}

这是我要实现的代码:

Employee myNewEmployee = DBClass.getEmployee("12345");
myNewEmployee.name = "John";
DBClass.AddEmployee(myNewEmployee);

但我显然收到以下异常:An entity object cannot be referenced by multiple instances of IEntityChangeTracker 因此,我被建议执行以下操作:

Employee myNewEmployee = DBClass.getEmployee("12345");
Employee myNewEmployee2 = new Employee();
// manually copy all the fields from myNewEmployee to myNewEmployee2
myNewEmployee2.name = "John";
DBClass.AddEmployee(myNewEmployee2);

但我认为它可能效率不高,因为我正在浪费时钟周期来拥有同一对象的相同副本。我们应该为整个应用程序使用一个静态上下文吗? (这是一个带有母版页的 ASPx 项目)。我在哪里可以阅读更多关于“如何使用上下文”的信息?非常感谢。

由于代码为每个调用使用完全独立的 DbContext 实例,因此 在技术上可以更新实例以在确保所有唯一性后将其复制并保存为新员工值和键被更新。但是,您现有的代码通过处置 DbContext 使实体跟踪引用成为孤立对象。类似 getEmployees 的代码应该使用 AsNoTracking() 加载处于分离状态的实体,或者在处理上下文之前从 DbContext 中分离实例。该方法也可以简化为删除 finally 块以使用 using 块处理处置:

static Employee getEmployee(string ssn)
{
    using(var context = new MyEntity())
    {
        try
        {
            return context.Employee.AsNoTracking().Single(X => X.ssn.Equals(ssn));
        }
        catch (Exception exc)
        {
            LogLibrary.WriteLog(exc.Message);
        }
    }
    return null;
}

或者您可以在 return 之前分离实体:

        try
        {
            var employee = context.Employee.Single(X => X.ssn.Equals(ssn));
            context.Entry(employee).State = EntityState.Detached;
            return employee;
        }

using 块负责处理范围内的实例。错过了一个 finally 块,并且您有一个未处理的 DbContext 实例。

当现在处置的 DbContext 跟踪初始实体时,您可以使用如下代码:

var employee = getEmployee("12345");
employee.SSN = "54321";
employee.Name = "John";
using(var context = new MyEntity())
{
    context.Employees.Add(employee);
    context.SaveChanges();
}

另一种方法是克隆 entity.There 有几种方法可以做到这一点,包括手动复制值或利用 Automapper 复制值。这可以配置为忽略复制键和唯一值。最基本的:

var config = new MapperConfiguration(cfg => cfg.CreateMap<Employee, Employee>());
var mapper = config.CreateMapper();

var sourceEmployee = getEmployee("12345");
var newEmployee = mapper.Map<Employee>(sourceEmployee);
newEmployee.SSN = "54321";
newEmployee.Name = "John";
using(var context = new MyEntity())
{
    context.Employees.Add(newEmployee);
    context.SaveChanges();
}

此代码需要确保更新主键值和任何唯一约束。如果员工 table 有一个 EmployeeId PK,并且该密钥被设置为一个身份,那么它应该被自动覆盖。否则,如果 PK 类似于 SSN,您需要在保存之前确保它是一个新的且唯一的值。为此,您应该首先检查数据库以确保新的 SSN 是唯一的:

using(var context = new MyEntity())
{
    if (!context.Employees.Any(x => x.SSN == newEmployee.SSN))
    {
        context.Employees.Add(newEmployee);
        context.SaveChanges();
    }
    else 
    {
        // handle that the new SSN is already in the database.
    }
}

关于仅使用单个静态 DbContext:不,对于 EF 这不是一个好主意。默认情况下,DbContexts 会跟踪它们加载的每个实例,除非明确告知不要这样做。这意味着它们存活的时间越长,它们跟踪的实例就越多,消耗内存并导致性能下降,因为 EF 将不断检查其已知的跟踪实例以查看它是否应该 return 而不是从数据库中提取新实例.在大多数情况下它仍然运行查询,因此处理跟踪实例并不会像您可能认为的将行为与缓存进行比较那样节省性能。通常您会希望多个调用与单个 DbContext 实例相关联,因此 DbContext 的范围过于精细会降低灵活性。例如,如果您想更新公司中的职位并将其与员工相关联,则使用 getEmployee() 方法将其范围限定为自己的 DBContext 实际上可能会产生意想不到的后果,例如此示例:

using (var context = new MyEntity())
{
    var position = context.Positions.Single(x => x.PositionId == positionId);
    var employee = getEmployee(ssn);
    position.Employee = employee;
    context.SaveChanges();
}

这可能最终导致在尝试插入新员工时出现重复约束的错误,或者它将插入员工记录的全新克隆。 (如果 Employee 为其 PK 配置了身份)原因是 Position 由 DbContext 的一个实例管理,而 getEmployee() 使用的是一个完全独立的 DbContext 实例。该职位不知道“员工”是现有记录,并将其视为全新记录。确保这些实例关联在一起的正确方法是确保它们都与同一个 DbContext 实例关联:

using (var context = new MyEntity())
{
    var position = context.Positions.Single(x => x.PositionId == positionId);
    var employee = context.Employees.Single(x => x.SSN == ssn);
    position.Employee = employee;
    context.SaveChanges();
}

或者确保此代码和 getEmployee 都被注入相同的 DbContext 实例,而不是在方法中限定它的范围。 (即依赖注入)像结构化代码一样使用分离实例是可能的,但它可能会变得非常混乱,所以要小心。