Игнорировать дублируемую вставку ключа с помощью Entity Framework

Я использую ASP.NET MVC4 с Entity Framework Code First. У меня есть таблица под названием "пользователи" с первичным ключом "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();

который будет работать. Проблема в том, что она должна запускать запрос 50 раз в 200 000 + таблице входа. Поэтому мой вопрос: какой эффективный способ вставки этих пользователей наиболее эффективен, игнорировать любые дубликаты?

Ответы

Ответ 1

Вы можете сделать это:

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.

Ответ 2

Вы можете отфильтровать существующих пользователей одним запросом

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

EDIT:

Как указано в комментариях, вышеприведенное предложение приведет к вызову sql для каждого элемента в коллекции NewUsers. Я могу подтвердить это с помощью SQL Server Profiler.

Один интересный результат профилирования - несколько более сложный sql, сгенерированный EF для каждого элемента (имена моделей отличаются от тех, что указаны в 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

Довольно приятный фрагмент кода, который выполняет работу простого однострочного интерфейса.

Моя точка зрения заключается в том, что писать хороший и читаемый декларативный код и позволить компилятору и оптимизатору выполнять грязную работу - это отличное отношение. Это один из тех случаев, когда результат такого стиля удивляет, и вы должны идти грязно.

Ответ 3

Поскольку это ваш первичный ключ, ваши варианты ограничены. Если это не ваш первичный ключ и просто уникальный индекс, предполагающий SQL Server, вы можете настроить свой уникальный ключ, чтобы игнорировать дубликаты.

То, что я могу предложить, - просто обернуть try/catch вокруг Add и есть исключение, если исключение является дубликатной ошибки ключа.

Вы также можете увидеть, поддерживает ли ваш объект метод AddOrUpdate(). Я знаю, что это поддерживается в реализациях Code First. Я считаю, что в этом случае он добавит новое или обновит, если строка существует. Тем не менее, это может по-прежнему включать в себя поездку в БД, чтобы узнать, существует ли пользователь уже для того, чтобы узнать, нужно ли добавлять или обновлять. И в некоторых случаях вам может не потребоваться выполнить обновление.

Я думаю, что если бы это был я, я бы пошел по маршруту Try/Catch.

Ответ 4

Следующий метод расширения позволит вам вставлять записи любого типа при игнорировании дубликатов:

 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;
        }
    }