Uniqie Constraint failed при попытке создать связь между новой и существующей сущностями
В контексте имеется два типа сущностей: MetadataObject - коллекция объектов, которые загружаются при старте программы и не подлежат редактированию пользователем. И DynamicObject - объекты могут редактироваться пользователем. Модель отношений - DynamicObject имеет один или ноль MetadataObject, MetadataObject имеет множество DynamicObject.
Контекст:
public class MeContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MetadataObject>()
.HasMany(x => x.DynamicObjects)
.WithOne(x => x.MetadataObject);
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite(@"Data Source=D:\\myDatabase.db");
public DbSet<DynamicObject> DynamicObjects { get; set; }
public DbSet<MetadataObject> MetadataObjects { get; set; }
}
public class DynamicObject
{
public int Id { get; set; }
public string Value { get; set; }
public int? MetadataObjectId { get; set; }
public MetadataObject MetadataObject { get; set; }
}
public class MetadataObject
{
public int Id { get; set; }
public string Value { get; set; }
public List<DynamicObject> DynamicObjects { get; set; }
}
Создаю базу данных и инициализирую метаданные:
class Program
{
static void Main(string[] args)
{
var meContext = new MeContext();
meContext.Database.EnsureDeleted();
meContext.Database.EnsureCreated();
meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 1" });
meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 2" });
meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 3" });
meContext.SaveChanges();
}
}
Пытаюсь создать экземпляр объекта DynamicObject, привязать к нему один из объектов метаданных и сохранить динамический объект в базе данных:
class Program
{
static void Main(string[] args)
{
var meContext = new MeContext();
List<MetadataObject> readonlyList = meContext.MetadataObjects
.AsNoTracking()
.ToList();
DynamicObject meObj = new DynamicObject() { Value = "dynamic value 1" };
//создание связи через навигационное свойство
meObj.MetadataObject = readonlyList[1];
meContext.DynamicObjects.Add(meObj);
//ошибка: Uniqie Constraint failed
meContext.SaveChanges();
}
}
Возникает указанная ошибка. Как я понимаю исключение возникает из за того, что EF пытается повторно добавить уже существующий объект метаданных с существующим Id обратно в контекст.
В следующем примере я создаю связь через внешнее свойство и запись в базу данных проходит нормально:
class Program
{
static void Main(string[] args)
{
var meContext = new MeContext();
//meContext.Database.EnsureDeleted();
//meContext.Database.EnsureCreated();
//meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 1" });
//meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 2" });
//meContext.MetadataObjects.Add(new MetadataObject() { Value = "metadata value 3" });
List<MetadataObject> readonlyList = meContext.MetadataObjects
.AsNoTracking()
.ToList();
DynamicObject meObj = new DynamicObject() { Value = "dynamic value 1" };
//создание связи через навигационное свойство
meObj.MetadataObject = readonlyList[1];
// Создание связи через внешний ключ
meObj.ReadOnlyObjectId = readonlyList[1].Id;
// Успешно
meContext.SaveChanges();
}
}
}
Не хотел бы создавать связи через манипуляции с ключами потому, что во-первых это неудобно, а во-вторых как мне кажется неправильно. Что я делаю не так?
Ответы (2 шт):
Не хотел бы создавать связи через манипуляции с ключами
Вообще-то, насколько мне известно, в базах данных все связи существуют только на основе ключей.
То есть, надо создать свойства DynamicObjectID и MetadataObjectID, потом что-то вроде:
DynamicObject meObj = new DynamicObject() { Value = "dynamic value 1" };
meObj.MetadataObjectID = meContext.MetadataObjects.Max(c => c.MetadataObjectID) + 1;
meObj.MetadataObject = meContext.MetadataObjects.Where(d => d.DynamicObjectID == 1);
meContext.Add(meObj;)
Ещё можешь попробовать в запрос добавить расширение .Include(), например,
using Microsoft.EntityFrameworkCore;
...
List<MetadataObject> readonlyList = meContext.MetadataObjects
.Include(m => m.DynamicObjects).AsNoTracking().ToList();
, если у тебя EntityFrameworkCore, он иногда сущности не подгружает, если явно не задать их в .Include(), в отличие от просто EntityFramework.
В общем я сам осознал свою проблему и находится она здесь:
List<MetadataObject> readonlyList = meContext.MetadataObjects
.AsNoTracking()
.ToList();
А именно в методе .AsNoTracking(). Изначально я добавил этот метод в запрос чтобы не дать пользователю вносить какие либо изменения данных внутри объектов MetadataObject. Даже если пользователь изменит один или несколько объектов MetadataObject, изменения не будут отслежены и в таблицу не попадут.
В итоге оказалось, что .AsNoTracking() не отслеживает не только данные, но и связи объекта. Такой объект становится невидимым для контекста. При добавлении связи EntityFramework пытается добавить такой объект как вновь создаваемый и натыкается на SQL ошибку, когда БД находит тот же объект с тем же первичным ключом. Таким образом .AsNoTracking() следует использовать не только когда данные внутри сущности не подлежат изменению, но и внешние связи тоже.