当我不知道记录是不是存在时,如何使用实体框架进行合并?

Posted

技术标签:

【中文标题】当我不知道记录是不是存在时,如何使用实体框架进行合并?【英文标题】:How can I use use Entity Framework to do a MERGE when I don't know if the record exists?当我不知道记录是否存在时,如何使用实体框架进行合并? 【发布时间】:2014-07-17 23:05:18 【问题描述】:

在this SO answer关于Entity Framework和MERGE的代码中,示例如下:

public void SaveOrUpdate(MyEntity entity)

  if (entity.Id == 0)
  
    context.MyEntities.AddObject(entity);
  
  else
  
    context.MyEntities.Attach(entity);
    context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
  

这假设您已经知道要更新的实体是否存在;在这种情况下,您检查entity.Id。但是,如果您不知道该项目是否存在怎么办?例如,在我的例子中,我将来自供应商的记录导入到我的数据库中,并且给定的记录可能已经导入,也可能尚未导入。如果记录存在,我想更新它,否则添加它。但是在这两种情况下都已经设置了供应商的 id。

除非我简单地询问数据库记录是否已经存在,否则我看不到任何方法,这违背了 MERGE 的全部目的。

【问题讨论】:

我认为简单的答案是 EF 无法执行 T-SQL MERGE 语句。添加和更新是唯一的选项,它总是需要数据库往返。您可以通过在开始导入之前获取所有 ID 来提高此往返效率。 【参考方案1】:

我在这种情况下使用AddOrUpdate。但是,我相信它确实会首先查询数据库以决定发出插入或更新。

context.MyEntities.AddOrUpdate(e => e.Id, entity);

更新:

我浏览了我的调试日志文件。首先它运行:

SELECT TOP (2) ... WHERE 1 = [Extent1].[Id]

然后它运行:

INSERT [dbo].[TestTable](...) VALUES (...)
SELECT [Id]
FROM [dbo].[TestTable]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()

或者:

UPDATE [dbo].[TestTable]
SET ...
WHERE ([Id] = @2)

更新 2: 这是一个使用 MERGE 的有趣扩展方法: https://gist.github.com/ondravondra/4001192

【讨论】:

你的意思是它发出两个查询,一个检查存在,一个做插入或更新? 我更新了我的回复以显示我看到 EF 生成的查询。是的,它在第一个查询中检查是否存在,然后决定插入或更新。 有趣。我认为如果它要发布两个声明,我宁愿自己处理这个问题。我将检查该扩展方法,因为这似乎可以解决问题。 (但为什么 EntityFramework 一开始就不能以这种方式工作,这似乎是一个明显的性能胜利?) 注意 AddOrUpdate 方法:thedatafarm.com/data-access/…【参考方案2】:

如果您想要一个没有存储过程的原子数据库 UPSERT 命令,并且您不担心上下文被更新,那么值得一提的是,您还可以将嵌入式 MERGE 语句包装在 ExecuteSqlCommand 调用中:

public void SaveOrUpdate(MyEntity entity)

    var sql =  @"MERGE INTO MyEntity
                USING 
                (
                   SELECT   @id as Id
                            @myField AS MyField
                ) AS entity
                ON  MyEntity.Id = entity.Id
                WHEN MATCHED THEN
                    UPDATE 
                    SET     Id = @id
                            MyField = @myField
                WHEN NOT MATCHED THEN
                    INSERT (Id, MyField)
                    VALUES (@Id, @myField);"

    object[] parameters = 
        new SqlParameter("@id", entity.Id),
        new SqlParameter("@myField", entity.myField)
    ;
    context.Database.ExecuteSqlCommand(sql, parameters);

这并不漂亮,因为它在 EF 对实体的抽象之外工作,但它允许您利用 MERGE 命令。

【讨论】:

这与我最终做的非常相似,所以我将其标记为答案。我还将它包装在一个通用的扩展方法中以便于重用。 有什么方法可以模拟这个 ExecuteSqlCommand,而不是模拟方法本身?【参考方案3】:

AddOrUpdate 是一个很好的解决方案,但它不是可扩展。需要一次数据库往返来检查实体是否已经存在,并且需要一次往返来插入或更新实体。因此,如果您保存 1000 个实体,将执行 2000 次数据库往返。

免责声明:我是项目的所有者Entity Framework Extensions

此库允许您在 Entity Framework 中执行合并操作,同时显着提高性能。保存 1000 个实体只需要 1 次数据库往返。

// Easy to use
context.BulkMerge(customers)

// Easy to customize
context.BulkMerge(customers, operation => 
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
);

【讨论】:

这将是一个很好的解决方案。但它不是免费的。它不能用于生产环境,因为免费下载版本需要每个月更新.... @user007 - 如果您想要同样快速和免费,那么您将不得不编写类似存储过程的东西,它采用 TableValuedParameter 并包含正确处理和锁定的 MERGE 语句,然后有效地更改您的 DAL使用它而不是 EF 来保存更改。如果您不想为这项工作而烦恼并且需要/想要将 EF 保留为 DAL,那么这个相当便宜的库是一个很棒的解决方案,因为它可以节省您的时间。【参考方案4】:

您可以更改 Entity Framework 生成的 INSERT 或 UPDATE sql 的唯一方法是 configure your model to use stored procedures。

然后您可以修改在 Up 迁移中生成的 sql 以创建插入和更新过程以使用您的 MERGE sql 而不是 INSERT 和 UPDATE

 CREATE PROCEDURE [dbo].[Blog_Insert]  
  @Name nvarchar(max),  
  @Url nvarchar(max)  
AS  
BEGIN 
  -- Your Merge Sql goes here

  --And you need to use MERGE OUTPUT to get the primary key 
  --instead of SCOPE_IDENTITY()
  --SELECT SCOPE_IDENTITY() AS BlogId 
END

【讨论】:

我认为插入存储过程不会起作用,因为它没有现有主键的参数。但是,是的,也许更新存储过程可以用于此。要尝试此操作,应将所有实体标记为Modified,并在存储过程中做出插入或更新的决定。我希望通过更新存储过程插入记录不会导致任何意外的副作用。 @GertArnold 我怀疑插入不需要现有的主键,因为 MERGE 使用的是“供应商 ID”——这不是主键——否则你不需要合并你呢? 这个问题并不完全清楚。 Id 可能是主键。无论如何,我认为使用这些程序之一是一个可行的选择。

以上是关于当我不知道记录是不是存在时,如何使用实体框架进行合并?的主要内容,如果未能解决你的问题,请参考以下文章

如何在实体框架模型中使用通用导航属性?

如何使用实体框架更新数据库中的记录时覆盖模型验证

如何在原始查询中使用实体框架事务?

使用 Django REST 框架进行批量 POST 时检查记录是不是存在

如何刷新实体框架核心 DBContext?

如何使用实体框架在文本字段中搜索空字符串?