忽略具有Entity Framework的重复键插入

时间:2013-08-07 20:42:04

标签: c# entity-framework

我正在使用ASP.NET MVC4和Entity Framework Code First。我有一个名为“users”的表,主键为“UserId”。此表可能有200,000多个条目。

我需要再插入50个用户。我可能会这样做

foreach(User user in NewUsers){
    context.Add(user);
}
dbcontext.SaveChanges();

问题是,这些新用户中的一个或多个可能已经存在于数据库中。如果我添加它们然后尝试保存,则会抛出错误,并且不会添加任何有效的错误。我可以修改代码来执行此操作:

foreach(User user in NewUsers){
    if(dbcontext.Users.FirstOrDefault(u => u.UserId) == null)
    {
        dbcontext.Users.Add(user);
    }
}
dbcontext.SaveChanges();

哪个会奏效。问题是,它必须在200,000+条目表上运行50次查询。所以我的问题是,插入这些用户的效率最高的方法是什么, 忽略 任何重复项?

5 个答案:

答案 0 :(得分:8)

你可以这样做:

var newUserIDs = NewUsers.Select(u => u.UserId).Distinct().ToArray();
var usersInDb = dbcontext.Users.Where(u => newUserIDs.Contains(u.UserId))
                               .Select(u => u.UserId).ToArray();
var usersNotInDb = NewUsers.Where(u => !usersInDb.Contains(u.UserId));
foreach(User user in usersNotInDb){
    context.Add(user);
}

dbcontext.SaveChanges();

这将在您的数据库中执行单个查询,以查找已存在的用户,然后将其从NewUsers集中过滤掉。

答案 1 :(得分:3)

您可以使用一个查询过滤掉现有用户

foreach(User user in NewUsers.Where(us => !dbcontext.Users.Any(u => u.userId == us.userId)))
{
    dbcontext.Users.Add(user);
}
dbcontext.SaveChanges();

编辑:

正如评论中所指出的,上面的提议将导致对NewUsers集合中的每个元素进行sql调用。我可以使用SQL Server Profiler确认。

分析的一个有趣的结果是EF为每个项目生成的有点奇怪的sql(模型名称与OP中的不同,但查询是相同的):

exec sp_executesql N'SELECT 
CASE WHEN ( EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[EventGroup] AS [Extent1]
    WHERE [Extent1].[EventGroupID] = @p__linq__0
)) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[EventGroup] AS [Extent2]
    WHERE [Extent2].[EventGroupID] = @p__linq__0
)) THEN cast(0 as bit) END AS [C1]
FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]',N'@p__linq__0 int',@p__linq__0=10

完成一个简单的单行代码的相当不错的代码。

我的观点是编写好的和可读的声明性代码并让编译器和优化器做脏工作是一种很好的态度。这是这种风格的结果令人惊讶并且你必须变脏的情况之一。

答案 2 :(得分:2)

由于这是您的主键,因此您的选项有限。如果这不是您的主键,而只是一个唯一索引,假定SQL Server,您可以设置唯一键来忽略重复项。

我可能建议简单地将一个try / catch包装在Add周围,如果异常是一个重复的键错误就吃异常。

您可能还会看到您的对象是否支持AddOrUpdate()方法。我知道Code First实现支持这一点。我相信在这种情况下,如果行存在,它将添加新的或更新。但是,这可能仍然需要访问数据库以查看用户是否已存在以便知道是否进行添加或更新。而且,在某些情况下,您可能不想实际执行更新。

我想如果是我,我会去Try / Catch路线。

答案 3 :(得分:0)

您可以过滤出故障并继续尝试直到成功,否则您将获得另一种异常

public partial class YourEntities: DbContext
{
    public override int SaveChanges()
    {
        var isSaved = false;
        do
        {
            try
            {
                return base.SaveChanges();
            }
            catch (DbUpdateException ex)
            {
                var entries = ex.Entries;
                foreach (var entry in entries)
                {
                    // change state to remove it from context 
                    entry.State = EntityState.Detached;
                }
            }
        }
        while (!isSaved);

        return null;    // never gets here
    }
}

您可能需要添加一些额外的逻辑来避免无限循环。

答案 4 :(得分:-1)

以下扩展方法允许您插入任何类型的记录,同时忽略重复:

 public static void AddRangeIgnore(this DbSet dbSet, IEnumerable<object> entities)
    {
        var entitiesList = entities.ToList();
        var firstEntity = entitiesList.FirstOrDefault();

        if (firstEntity == null || !firstEntity.HasKey() || firstEntity.HasIdentityKey())
        {
            dbSet.AddRange(entitiesList);
            return;
        }

        var uniqueEntities = new List<object>();

        using (var dbContext = _dataService.CreateDbContext())
        {
            var uniqueDbSet = dbContext.Set(entitiesList.First().GetType());

            foreach (object entity in entitiesList)
            {
                var keyValues = entity.GetKeyValues();
                var existingEntity = uniqueDbSet.Find(keyValues);

                if (existingEntity == null)
                {
                    uniqueEntities.Add(entity);
                    uniqueDbSet.Attach(entity);
                }
            }
        }

        dbSet.AddRange(uniqueEntities);
    }

    public static object[] GetKeyValues(this object entity)
    {
        using (var dbContext = _dataService.CreateDbContext())
        {
            var entityType = entity.GetType();
            dbContext.Set(entityType).Attach(entity);
            var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity);
            var value = objectStateEntry.EntityKey
                                        .EntityKeyValues
                                        .Select(kv => kv.Value)
                                        .ToArray();
            return value;
        }
    }

    public static bool HasKey(this object entity)
    {
        using (var dbContext = _dataService.CreateDbContext())
        {
            var entityType = entity.GetType();
            dbContext.Set(entityType).Attach(entity);
            var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity);
            return objectStateEntry.EntityKey != null;
        }
    }

    public static bool HasIdentityKey(this object entity)
    {
        using (var dbContext = _dataService.CreateDbContext())
        {
            var entityType = entity.GetType();
            dbContext.Set(entityType).Attach(entity);
            var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity);
            var keyPropertyName = objectStateEntry.EntityKey
                                        .EntityKeyValues
                                        .Select(kv => kv.Key)
                                        .FirstOrDefault();

            if (keyPropertyName == null)
            {
                return false;
            }

            var keyProperty = entityType.GetProperty(keyPropertyName);
            var attribute = (DatabaseGeneratedAttribute)Attribute.GetCustomAttribute(keyProperty, typeof(DatabaseGeneratedAttribute));
            return attribute != null && attribute.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity;
        }
    }