Entity Framework DBContext全局缓存?

3

(EF4.1 - 4.0框架)

网络上的大部分代码示例都规定了Entity Framework的最佳实践;它们说要在using块中包装DBContext的使用,以确保无状态操作。即使如此,我仍然遇到了似乎是共享缓存错误的问题。

错误:

对象StateManager中已经存在具有相同键的对象。 ObjectStateManager无法跟踪具有相同键的多个对象。

查看了一下,发现这种情况的示例是当某人在多个调用之间共享全局DBContext实例时发生的。

然而,我在一个静态数据访问层服务类中的以下函数的第二次调用中收到了这个错误信息。

public static void UpdateRollout(Rollout rollout)
        {

               using (ITAMEFContext db = new ITAMEFContext(ConnectionStrings.XYZConnectionString))
                {
                    db.Configuration.ProxyCreationEnabled = false;
                    db.Configuration.LazyLoadingEnabled = false;

                    FixUp(rollout);


                    db.Rollouts.Attach(rollout);
                    db.Entry(rollout).State = System.Data.EntityState.Modified;

                    db.SaveChanges();

                    //db.Entry(rollout).State = System.Data.EntityState.Detached;

                }

}



private static void FixUp(Rollout rollout)
        {
            // ensure manual fixup of foreign keys
            if (rollout.RolloutState != null)
                rollout.FK_RolloutState_ID = rollout.RolloutState.ID;
            if (rollout.Lead != null)
                rollout.RolloutLead_FK_User_ID = rollout.Lead.ID;
        }

EFContext是通过EF 4.x DBContext Fluent Generator生成的,该生成器引用了一个edmx模型。 edmx模型图片 看起来像这样。
public partial class ITAMEFContext : DbContext
{
    static ITAMEFContext()
    {
        Database.SetInitializer<ITAMEFContext>(null);
    }

    public ITAMEFContext() : base("name=ITAMEFContext")
    {
        this.Configuration.LazyLoadingEnabled = false;

    }

    public ITAMEFContext(string nameOrConnectionString) : base(nameOrConnectionString)
    {

    }

    public ITAMEFContext(string nameOrConnectionString, DbCompiledModel model) : base(nameOrConnectionString, model)
    {

    }

    public ITAMEFContext(DbConnection existingConnection, bool contextOwnsConnection) : base(existingConnection, contextOwnsConnection)
    {

    }

    public ITAMEFContext(DbConnection existingConnection, DbCompiledModel model, bool contextOwnsConnection) : base(existingConnection, model, contextOwnsConnection)
    {

    }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
        modelBuilder.Configurations.Add(new Asset_Mapping());
        modelBuilder.Configurations.Add(new AssetAllocation_Mapping());
        modelBuilder.Configurations.Add(new AssetAssignee_Mapping());
        modelBuilder.Configurations.Add(new AssetAssigneeType_Mapping());
        modelBuilder.Configurations.Add(new AssetDeAllocation_Mapping());
        modelBuilder.Configurations.Add(new AssetState_Mapping());
        modelBuilder.Configurations.Add(new AssetType_Mapping());
        modelBuilder.Configurations.Add(new Department_Mapping());
        modelBuilder.Configurations.Add(new Location_Mapping());
        modelBuilder.Configurations.Add(new ManagementGroup_Mapping());
        modelBuilder.Configurations.Add(new Role_Mapping());
        modelBuilder.Configurations.Add(new Rollout_Mapping());
        modelBuilder.Configurations.Add(new RolloutState_Mapping());
        modelBuilder.Configurations.Add(new ServiceArea_Mapping());
        modelBuilder.Configurations.Add(new Software_Mapping());
        modelBuilder.Configurations.Add(new SoftwareType_Mapping());
        modelBuilder.Configurations.Add(new SubTeam_Mapping());
        modelBuilder.Configurations.Add(new sys_UserLock_Mapping());
        modelBuilder.Configurations.Add(new Team_Mapping());
        modelBuilder.Configurations.Add(new User_Mapping());
        modelBuilder.Configurations.Add(new WorkingMethod_Mapping());
    }

    public DbSet<Asset> Assets { get; set; }
    public DbSet<AssetAllocation> AssetAllocations { get; set; }
    public DbSet<AssetAssignee> AssetAssignees { get; set; }
    public DbSet<AssetAssigneeType> AssetAssigneeTypes { get; set; }
    public DbSet<AssetDeAllocation> AssetDeAllocations { get; set; }
    public DbSet<AssetState> AssetStates { get; set; }
    public DbSet<AssetType> AssetTypes { get; set; }
    public DbSet<Location> Locations { get; set; }
    public DbSet<Department> Departments { get; set; }
    public DbSet<ManagementGroup> ManagementGroup { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<ServiceArea> ServiceAreas { get; set; }
    public DbSet<SubTeam> SubTeams { get; set; }
    public DbSet<Team> Teams { get; set; }
    public DbSet<User> User { get; set; }
    public DbSet<WorkingMethod> WorkingMethods { get; set; }
    public DbSet<Rollout> Rollouts { get; set; }
    public DbSet<RolloutState> RolloutStates { get; set; }
    public DbSet<Software> Softwares { get; set; }
    public DbSet<SoftwareType> SoftwareTypes { get; set; }
    public DbSet<sys_UserLock> sys_UserLock { get; set; }
}

我希望能够在我的BL层中随时调用UpdateRollout。UI需要保持对先前获取的List返回的纯POCO Rollout实体图形的掌握。
Rollout和所有其他实体都是纯POCO,不需要上下文跟踪。
我读到过,任何上下文缓存/跟踪都会在using块销毁ITAMEFContext后被清除。然而,似乎有某种全局缓存作为同一应用程序域中任何DBContext实例的基础?老实说,到目前为止,EF似乎比使用好老式存储过程的分层应用程序更费力。
这是POCO。
public partial class Rollout
{
    public Rollout()
    {
        this.AssetAssignees = new HashSet<AssetAssignee>();
    }

    public int ID { get; set; }
    public string Name { get; set; }
    public int RolloutLead_FK_User_ID { get; set; }
    public string EmailContacts { get; set; }
    public System.DateTime Schedule { get; set; }
    public int FK_RolloutState_ID { get; set; }
    public Nullable<int> NotificationDays { get; set; }
    public string Notes { get; set; }

    public virtual ICollection<AssetAssignee> AssetAssignees { get; set; }
    public virtual User Lead { get; set; }
    public virtual RolloutState RolloutState { get; set; }
}

编辑:

映射。

 internal partial class Rollout_Mapping : EntityTypeConfiguration<Rollout>
{
    public Rollout_Mapping()
    {                   
        this.HasKey(t => t.ID);     
        this.ToTable("Rollout");
        this.Property(t => t.ID).HasColumnName("ID");
        this.Property(t => t.Name).HasColumnName("Name").IsRequired().HasMaxLength(50);
        this.Property(t => t.RolloutLead_FK_User_ID).HasColumnName("RolloutLead_FK_User_ID");
        this.Property(t => t.EmailContacts).HasColumnName("EmailContacts").HasMaxLength(500);
        this.Property(t => t.Schedule).HasColumnName("Schedule");
        this.Property(t => t.FK_RolloutState_ID).HasColumnName("FK_RolloutState_ID");
        this.Property(t => t.NotificationDays).HasColumnName("NotificationDays");
        this.Property(t => t.Notes).HasColumnName("Notes");
        this.HasRequired(t => t.Lead).WithMany(t => t.Rollouts).HasForeignKey(d => d.RolloutLead_FK_User_ID);
        this.HasRequired(t => t.RolloutState).WithMany(t => t.Rollouts).HasForeignKey(d => d.FK_RolloutState_ID);
    }
}

在附加EntityState之前,请检查rollout的状态。如果已附加,请先将其分离(如果将导航属性设置为已跟踪的实体,则新实体将自动添加)。 - VahidN
2个回答

0

我遇到了一个非常相似的问题,就像你一样,我认为这是某种全局缓存引起的问题。

我的用例是这样的:

  1. 使用新的DbContext,在我的数据库上设置一些测试数据,然后处理DbContet
  2. 在我的应用程序上运行系统测试
  3. 将数据库重置为基线状态(我是在EF之外进行的)
  4. 从步骤1开始重复下一个系统测试

第一个测试运行得很好,但在第二个测试中,我遇到了重复键错误。

这让我困惑了一段时间,直到我意识到我正在使用的一个工厂方法来构建一些我的测试数据实体作为静态对象创建它们;第二次通过循环时,只要我将这些静态实体添加到上下文中,这些实体的完整对象图就会被重新添加,因此当我稍后再添加其他实体时,它们已经存在。

这里是一个简化的例子...

循环1:

  1. 创建对象A(静态)。保存更改[数据库现在包含A]
  2. 创建对象B(非静态),它与对象A有关系。保存更改[数据库现在包含A和B]
  3. 重置数据库[数据库现在不包含任何内容]

循环2:

  1. 创建对象A(静态,因此实际上没有重新创建。即使B不在数据库中,仍然包含对B的引用)。保存更改[数据库现在包含A和B]
  2. 创建对象B(非静态)。保存更改。[BOOM!重复键,因为B已经在数据库中]

解决方案:我更改了我的工厂方法,以便我的所有实体都不是静态的。问题得到解决。


-1

编辑 - 我已经重写了我的答案。两点。

1:我找到了这篇关于EF DbContext生命周期的文章(它涉及ObjectContext,但同样的规则适用): http://blogs.msdn.com/b/alexj/archive/2009/05/07/tip-18-how-to-decide-on-a-lifetime-for-your-objectcontext.aspx

请注意,DbContext不是线程安全的。由于您正在使用静态方法,可能会遇到线程问题。在需要时创建DbContext而不是在静态类中进行创建可能是值得的。

2:理想情况下,您应该在同一个DbCntext实例中读取和写入。 "断开连接"只是意味着在处理实体时它们在内存中,并且DbContext正在跟踪您所做的更改。

我们使用更像这样的方法(伪代码):

 public class RolloutManager {
   ...
   // If you just update state and you have no input from somewhere else, you can just  
   // read and write in the same method
   public void UpdateRolloutState() {
       using( var db = new MyDBContext() {
            var stuffToUpdate = db.Rollouts.Where(....).ToList();
            foreach(var stuff in StuffToUpdate){
                stuff.PropertyToUpdate = ....;
            }
            db.SaveChanges();
        }
   }

   // If you have inputs, pass them in (using a different object normally, such as a wcf 
   //contract or viewmodel), read them up from the db, update the db entities and save
   public void UpdateRolloutState(IEnumerable<InputRollout> stuffToUpdate) {
       using( var db = new MyDBContext() {
            foreach(var stuff in StuffToUpdate){
                var dbRollout = db.Rollouts.Find(stuff.Id);
                // copy properties you want to update
            }
            db.SaveChanges();
        }
   }

希望这可以帮到你 - 它可能不是解决方案,但它可能指引你找到一个。


但是你如何解释第一次调用 UpdateRollout 正常运行的情况呢?你的注释 "This object is probably already there" 没有解释核心问题。此外,Find 将使不必要的往返数据库,而 OP 尝试通过附加来防止这种情况。并且 rollout 本身的任何更改都没有保存。 得分 -1,抱歉。 - Gert Arnold
如果在这个函数中使用相同的上下文,你需要检查它是否已经被缓存并更新属性或使用Find,我完全理解。然而,每次调用此函数都会导致上下文被释放。因此,第二次调用应该是一个新的上下文。这表明DBContext在底层工作中存在一些隐藏的行为。无论DBContext被释放多少次,它都必须以某种方式维护状态,需要开发人员去发现这一点,并采用与一些人建议的方法截然不同的方法来处理。 - Microsoft Developer
令人困惑的是,根据所有记录,DBContext.Find将不得不去数据库检索记录,因为它不应该在DBContext对象图缓存中,因为上次使用后已被处理,因此使用附加可以节省一次往返,但缺点是您必须管理状态。所以你必须'attach',这将标记图中的所有子对象为“未更改”,然后您应该能够选择相应地更新哪些对象。我认为这是您可以通过EF在层/层之间真正“断开连接”的唯一方法。我并不是EF方面的专家 :/ - Microsoft Developer
经过更多的研究,我重写了我的答案,并添加了我们通常如何处理DbContext(没有静态方法)以及有关DbContext不是线程安全的文章,这可能是问题所在。希望这能帮助你开始尝试不同的解决方案。 - gabnaim
有一件事突然想到我了。请确保在您的“rollout”对象中初始化密钥并且它是唯一的。另外,错误可能不在rollout上...它可能是rolloutState吗? - gabnaim
显示剩余3条评论

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