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 шт):

Автор решения: Ivan Silkin

Не хотел бы создавать связи через манипуляции с ключами

Вообще-то, насколько мне известно, в базах данных все связи существуют только на основе ключей.

То есть, надо создать свойства 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.

→ Ссылка
Автор решения: Igor Cheglakov

В общем я сам осознал свою проблему и находится она здесь:

    List<MetadataObject> readonlyList = meContext.MetadataObjects
        .AsNoTracking()
        .ToList();

А именно в методе .AsNoTracking(). Изначально я добавил этот метод в запрос чтобы не дать пользователю вносить какие либо изменения данных внутри объектов MetadataObject. Даже если пользователь изменит один или несколько объектов MetadataObject, изменения не будут отслежены и в таблицу не попадут. В итоге оказалось, что .AsNoTracking() не отслеживает не только данные, но и связи объекта. Такой объект становится невидимым для контекста. При добавлении связи EntityFramework пытается добавить такой объект как вновь создаваемый и натыкается на SQL ошибку, когда БД находит тот же объект с тем же первичным ключом. Таким образом .AsNoTracking() следует использовать не только когда данные внутри сущности не подлежат изменению, но и внешние связи тоже.

→ Ссылка