【问题标题】:How do I upsert a record in ADO.NET EF 4.1?如何在 ADO.NET EF 4.1 中插入记录?
【发布时间】:2011-06-28 17:52:14
【问题描述】:

我正在尝试完成一些非常简单的事情,但我找不到使用 Entity Framework 4.1 的方法。

我想要一个控制器方法,它接受一个对象,然后执行 UPSERT(插入或更新,具体取决于数据库中是否存在记录)。

我使用的是自然密钥,所以我无法查看我的 POCO 并判断它是否是新的。

这就是我的做法,但对我来说似乎是错误的:

[HttpPost]
public JsonResult SaveMyEntity(MyEntity entity)
{            
    MyContainer db = new MyContainer(); // DbContext
    if (ModelState.IsValid)
    {
        var existing =
            db.MyEntitys.Find(entity.MyKey);
        if (existing == null)
        {
            db.MyEntitys.Add(entity);
        }
        else
        {
            existing.A = entity.A;
            existing.B = entity.B;
            db.Entry(existing).State = EntityState.Modified;
        }
        db.SaveChanges();
        return Json(new { Result = "Success" });
    }
}

理想情况下,整个事情应该是这样的:

db.MyEntities.AddOrModify(entity);

【问题讨论】:

  • 只做Save会发生什么?

标签: c# entity-framework-4.1


【解决方案1】:

不幸的是,如果不查询数据库或使用存储过程,就无法做到这一点。简约的代码应该是:

public void AddOrModify<T>(T entity, string key) where T : class, IEntity // Implements MyKey 
{
     using (var context = new MyContainer())
     {
         if (context.Set<T>().Any(e => e.MyKey == key))
         {
              context.Entry(entity).State = EntityState.Modified;
         } 
         else
         {
              context.Entry(entity).State = EntityState.Added;
         }

         context.SaveChanges();
     }
}

【讨论】:

  • 这行得通。框架发出的 Sql 并不理想,但至少它省去了我在需要更新时手动设置字段的麻烦。谢谢。
  • 我建议您针对竞争条件对此进行压力测试(请参阅下面的 SP 建议)。
  • 只要确保事务的原子性,就可以在线路的任一侧执行冲突检测。当您处理多个数据存储时,这一点尤其重要。服务器端解决方案应该总是更有效,但不是必需的。这个答案可能是通过 DbContext API 进行 upsert 的最干净的示例之一。
  • 只做Save会发生什么?
  • 这个方法在我看来名称不正确。
【解决方案2】:

在大多数情况下,您不需要显式设置 EntityState.Modified,除非您已禁用更改跟踪。

我们采取的解决方案是检查实体标识符的值:

if (entity.Id == default(int)) {
    // transient entity so insert
} else {
    // update
}

【讨论】:

  • 这个解决方案对我不起作用,因为我没有使用整数身份密钥。我使用的是自然复合键,所以我无法查看我的 POCO 并判断它是否是新的。
  • 然后点击数据库进行检查似乎是您唯一可行的解​​决方案。最好有明确定义的插入/更新操作,实际上您应该使用视图模型,但我很欣赏这可能是一个简单的例子。
  • 您可以考虑缓存一组(线程安全的)使用过的标识符并在每次插入/删除操作后更新它(除非您可以更改密钥,但在我看来这是非常错误的)以加快过程,但是如果您不使用自动生成的密钥(这是要走的路,为什么不使用它们,您不必删除您的密钥,您甚至可以使用唯一约束对其进行索引)这是我唯一的方法见(处理重复键异常并不是真正的方法:D)
  • @luckyluke 错了,你应该总是处理重复键异常,除非你总是从单个线程写入数据库。否则在使用 EF 时总会出现竞态条件,因为它不支持 MERGE SQL 操作。
【解决方案3】:

实际上有一种方法可以通知数据库上下文,您尝试插入的实体已更改,现在是新的

_context.MyEntity.Attach(entity);
_context.MyEntity(entity).State = System.Data.EntityState.Modified;

【讨论】:

    【解决方案4】:

    要执行 UPSERT 操作,您可能需要考虑创建一个执行 MERGE 的 SP。

    http://www.databasejournal.com/features/mssql/article.php/3739131/UPSERT-Functionality-in-SQL-Server-2008.htm

    无论您选择何种方式,操作都必须是原子的,否则您将遇到竞争条件。 你的 SP 可能需要一个 HOLDLOCK 来取消这个 ...

    http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

    【讨论】:

    • 只做Save会发生什么?
    【解决方案5】:

    我知道这个问题很老,并且有一个可以接受的答案,但我认为有更好的解决方案:它不需要实现额外的接口或定义键类型。

    public static class DbSetExtensions
    {
        public static EntityEntry<TEnt> AddIfNotExists<TEnt, TKey>(this DbSet<TEnt> dbSet, TEnt entity, Func<TEnt, TKey> predicate) where TEnt : class
        {
            var exists = dbSet.Any(c => predicate(entity).Equals(predicate(c)));
            return exists
                ? null
                : dbSet.Add(entity);
        }
    
        public static void AddRangeIfNotExists<TEnt, TKey>(this DbSet<TEnt> dbSet, IEnumerable<TEnt> entities, Func<TEnt, TKey> predicate) where TEnt : class
        {
            var entitiesExist = from ent in dbSet
                where entities.Any(add => predicate(ent).Equals(predicate(add)))
                select ent;
    
            dbSet.AddRange(entities.Except(entitiesExist));
        }
    }
    

    所以以后可以这样使用:

    using (var context = new MyDbContext())
    {
        var user1 = new User { Name = "Peter", Age = 32 };
        context.Users.AddIfNotExists(user1, u => u.Name);
    
        var user2 = new User { Name = "Joe", Age = 25 };
        context.Users.AddIfNotExists(user2, u => u.Age);
    
        // Adds user1 if there is no user with name "Peter"
        // Adds user2 if there is no user with age 25
        context.SaveChanges();
    }
    

    【讨论】:

    • @GertArnold 实际上我正在寻找“UPSERT”或“如果实体已经存在则插入或不执行任何操作”,但我发现的所有解决方案都要求为每个实体实现接口或使用专门的方法类型等。所以我创建了这个在我的应用程序中使用的通用方法,是的,它有效。
    • 有效吗?代码甚至无法编译!如果是这样,您将得到“LINQ to Entities 不支持 LINQ 表达式节点类型 'Invoke'。”
    • @GertArnold 可能取决于 VS 和 C# 的版本(我使用的是 VS 2017 Community 和 C# 7.1)和 .NET Framework 和 EntityFramework 库。事实上,我没有像 2011 年在 OP 中那样使用 EF 4.1。你使用的是什么 EF 版本(.NET 是“完整”框架还是核心?)
    • 您显然是针对 ef-core,这与 ef 4.1 - 6 完全不同,正如问题所预期的那样。但是,即使对于 ef-core,这也是一个糟糕的解决方案,因为它会强制客户端评估,即在应用谓词之前将整个表拉入内存。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-28
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 1970-01-01
    • 2023-04-07
    相关资源
    最近更新 更多