Добавление и обновление взаимосвязанных сущностей в EF6
Используется Entity Framework 6.4.4. Имеется объект Profile с вложенными коллекциями/объектами, который добавляется/обновляется в транзакции. Выбор объекта из базы данных выполняется с AsNoTracking(). Пользователь получает в редакторе объект Profile и может удалять, добавлять и редактировать сущности без добавления в БД.
Проблема заключается в том, что если добавлять или удалять объекты по одному, либо добавлять полностью новый объект Profile со всеми вложенными коллекциями, то всё работает как и ожидается, но если добавить, например сразу два новых объекта Column к уже сохранённой в БД Sheet, то выбрасывается исключение:
System.InvalidOperationException:
Attaching an entity of type 'XXX.Data.Column' failed because another entity of the same type already has the same primary key value.
This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values.
This may be because some entities are new and have not yet received database-generated key values.
In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
Исключение выбрасывается при попытке установить состояние сущности db.Entry(entity).State при добавлении нескольких новых объектов, причём при первой же попытке, даже у уже существующей сущности. Но если я меняю код так:
// db.Entry(column).State = column.Id == 0
// ? EntityState.Added
// : EntityState.Modified;
if (column.Id == 0)
{
db.Columns.Add(column);
}
, то при добавлении нескольких сущностей всё сохраняется без ошибок, но в этом случае, конечно, игнорируются изменения в уже существующих сущностях. Также пробовал вариант с использованием Find и последующей проверкой на null так:
var _column = db.Columns.Find(column.Id);
if(_column == null)
{
db.Columns.Add(column);
}
else
{
db.Entry(_column).State = EntryState.Modified;
}
Пробовал отключать в dbContext.Configuration автоопределение изменений, проверку при сохранении (в надежде на более детальное описание ошибки) и ленивую загрузку, но также безрезультатно.
Пробовал устанавливать у всех сущностей состояние в Detached через ChangeTracker, также безрезультатно.
Пятый день "пляшу с бубном", но понимаю, что я что-то не понимаю...
Почему так происходит? Каким образом и добавить, и обновить сущности?
Упрощенная структура классов:
public partial class Profile
{
public Profile()
{
Sheets = new HashSet<Sheet>();
}
public int Id { get; set; }
public virtual ICollection<Sheet> Sheets { get; set; }
}
public partial class Sheet
{
public Sheet()
{
Columns = new HashSet<Column>();
}
public int Id { get; set; }
public int ProfileId { get; set; }
public virtual Profile Profile { get; set; }
public virtual ICollection<Column> Columns { get; set; }
}
public partial class Column
{
public int Id { get; set; }
public int SheetId { get; set; }
public virtual Sheet Sheet { get; set; }
public int PropertyItemId { get; set; }
public virtual PropertyItem Property { get; set; }
}
public partial class PropertyItem
{
public PropertyItem()
{
Columns = new HashSet<Column>();
}
public int Id { get; set; }
public virtual ICollection<Column> Columns { get; set; }
}
// Fluent API
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Column>()
.HasMany(e => e.Statements)
.WithRequired(e => e.Column)
.HasForeignKey(e => e.ColumnId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Sheet>()
.HasMany(e => e.Columns)
.WithRequired(e => e.Sheet)
.HasForeignKey(e => e.SheetId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Profile>()
.HasMany(e => e.Sheets)
.WithRequired(e => e.Profile)
.HasForeignKey(e => e.ProfileId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<PropertyItem>()
.HasMany(e => e.Columns)
.WithRequired(e => e.PropertyItem)
.HasForeignKey(e => e.PropertyItemId)
.WillCascadeOnDelete(false);
}
Для добавления/обновления используется такой-вот "костыль":
// File Profile.Utils.cs
public void AddOrUpdateProfiles(ICollection<Profile> profiles)
{
using (var db = new DbModel())
using (var transaction = db.Database.BeginTransaction())
{
try
{
db.Profiles.RemoveMissingItems(profiles, _ => true, db);
foreach (var profile in profiles)
{
db.Sheets.RemoveMissingItems(profile.Sheets, x => x.ProfileId == profile.Id, db);
foreach (var sheet in profile.Sheets)
{
db.Columns.RemoveMissingItems(sheet.Columns, x => x.SheetId == sheet.Id, db);
foreach (var column in sheet.Columns)
{
db.Statements.RemoveMissingItems(column.Statements, x => x.ColumnId == column.Id, db);
foreach (var statement in column.Statements)
{
db.Entry(statement).State = statement.Id == 0
? EntityState.Added
: EntityState.Modified;
}
db.Entry(column).State = column.Id == 0
? EntityState.Added
: EntityState.Modified;
}
db.Entry(sheet).State = sheet.Id == 0
? EntityState.Added
: EntityState.Modified;
}
db.Entry(profile).State = profile.Id == 0
? EntityState.Added
: EntityState.Modified;
}
db.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw;
}
}
}
Расширение для DbSet'a просто устанавливает состояние удалённых сущностей:
public static void RemoveMissingItems<T>(this DbSet<T> dbSet,
IEnumerable<T> source,
Expression<Func<T, bool>> where,
DbModel db) where T : class, IEntity
{
dbSet.AsNoTracking()
.Where(where)
.AsNoTracking()
.AsEnumerable()
.Where(x => !source.Any(y => y.Id == x.Id))
.ForEach(z =>
{
db.Entry(z).State = EntityState.Deleted;
});
}
UPDATE
На данный момент делаю так:
if(column.Id == 0)
{
db.Columns.Add(column);
}
else
{
var _column = db.Columns.Find(column.Id);
_column.PropertyOne = column.PropertyOne;
_column.PropertyTwo = column.PropertyTwo;
// ... and so on
}
Работает без ошибок, но всё таки хотелось бы разобраться в чём причина, чтоб не "выплясывать" в следующий раз.