Игнорировать дублируемую вставку ключа с помощью 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;
}
}