Отношение "один ко многим" в EF Core / Sqlite не удается из-за ограничения уникального индекса

В контексте каждого Car есть соответствующий CarBrand. Теперь мои классы выглядят так, как показано ниже:

public class Car
{
    public int CarId { get; set; }
    public int CarBrandId { get; set; }
    public CarBrand CarBrand { get; set; }
}

public class CarBrand
{
    public int CarBrandId { get; set; }
    public string Name { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }
    public DbSet<CarBrand> CarBrands { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data Source = MyDatabase.sqlite");
    }
}

Вот пример выполнения моего кода ...

class Program
{
    static void Main(string[] args)
    {
        AlwaysCreateNewDatabase();

        //1st transaction
        using (var context = new MyContext())
        {
            var honda = new CarBrand() { Name = "Honda" };
            var car1 = new Car() { CarBrand = honda };
            context.Cars.Add(car1);
            context.SaveChanges();
        }

        //2nd transaction
        using (var context = new MyContext())
        {
            var honda = GetCarBrand(1);
            var car2 = new Car() { CarBrand = honda };
            context.Cars.Add(car2);
            context.SaveChanges(); // exception happens here...
        }
    }

    static void AlwaysCreateNewDatabase()
    {
        using (var context = new MyContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
        }
    }

    static CarBrand GetCarBrand(int Id)
    {
        using (var context = new MyContext())
        {
            return context.CarBrands.Find(Id);
        }
    }
}

Проблема в том, что я получаю исключение 'UNIQUE constraint failed: CarBrands.CarBrandId', когда car2 добавляется в базу данных с тем же CarBrand honda.

Я ожидаю, что он сделает это во время второй транзакции context.SaveChanges(), он добавит car2 и соответствующим образом установит связь с CarBrand, но вместо этого я получаю исключение.

РЕДАКТИРОВАТЬ: мне действительно нужно получить мой экземпляр CarBrand в другом контексте / транзакции.

        //really need to get CarBrand instance from different context/transaction
        CarBrand hondaDb = null;
        using (var context = new MyContext())
        {
            hondaDb = context.CarBrands.First(x => x.Name == "Honda");
        }

        //2nd transaction
        using (var context = new MyContext())
        {
            var car2 = new Car() { CarBrand = hondaDb };
            context.Cars.Add(car2);
            context.SaveChanges(); // exception happens here...
        }

person Jan Paolo Go    schedule 19.04.2017    source источник
comment
Не уверен, что это является причиной проблемы, но обычно я не вижу статики в операциях CRUD.   -  person David Lee    schedule 19.04.2017
comment
Почему вы должны каждый раз использовать другой контекст?   -  person David Lee    schedule 19.04.2017
comment
допустим, я перезапустил приложение. тогда это было бы в другом контексте, не так ли?   -  person Jan Paolo Go    schedule 19.04.2017


Ответы (2)


Проблема в том, что Add каскады методов:

Начинает отслеживать данную сущность, и любые другие достижимые сущности, которые еще не отслеживаются, в состоянии Added, так что они будут вставлены в базу данных при вызове SaveChanges.

Есть много способов достичь цели, но наиболее гибкий (и я думаю, что предпочтительный) - заменить вызов метода Add на "метод _EntityEntryGracking_EntityNode" 5:

Начинает отслеживать сущность и любые сущности, к которым можно получить доступ, просматривая ее свойства навигации. Обход является рекурсивным, поэтому свойства навигации любых обнаруженных объектов также будут сканироваться. Указанный обратный вызов вызывается для каждой обнаруженной сущности и должен устанавливать состояние, в котором должна отслеживаться каждая сущность. Если состояние не установлено, сущность остается неотслеживаемой. Этот метод разработан для использования в отключенных сценариях, где объекты извлекаются с использованием одного экземпляра контекста, а затем изменения сохраняются с использованием другого экземпляра контекста. Примером этого является веб-служба, в которой одна служба call извлекает сущности из базы данных, а другой вызов службы сохраняет любые изменения в сущностях. Каждый вызов службы использует новый экземпляр контекста, который удаляется после завершения вызова. Если обнаруживается сущность, которая уже отслеживается контекстом, эта сущность не обрабатывается (и ее свойства навигации не просматриваются).

Поэтому вместо context.Cars.Add(car2); вы можете использовать следующее (оно довольно общее и должно работать почти во всех сценариях):

context.ChangeTracker.TrackGraph(car2, node =>
    node.Entry.State = !node.Entry.IsKeySet ? EntityState.Added : EntityState.Unchanged);
person Ivan Stoev    schedule 19.04.2017

Быстрая починка:

У EntityFramework Core проблема с вашим кодом, использующим новый Context внутри другого. Вы смешиваете состояния сущностей между ними двумя. Просто используйте один и тот же контекст для получения и создания. Если вы идете с:

using (var context = new MyContext())
{
    var honda = context.CarBrands.Find(1);
    var car2 = new Car() { CarBrand = honda };
    context.Cars.Add(car2);
    context.SaveChanges(); //fine now
    var cars = context.Cars.ToList(); //will get two
}

все должно быть хорошо.


Если вы действительно хотите получить для двух контекстов

    static void Main(string[] args)
    {
        AlwaysCreateNewDatabase();

        CarBrand hondaDb = null;
        using (var context = new MyContext())
        {
            hondaDb = context.CarBrands.Add(new CarBrand {Name = "Honda"}).Entity;
            context.SaveChanges();
        }

        using (var context = new MyContext())
        {
            var car2 = new Car() { CarBrandId = hondaDb.CarBrandId };
            context.Cars.Add(car2);
            context.SaveChanges();
        }
    }

Теперь вы не зависите от механики отслеживания изменений, а скорее от простого отношения FOREIGN KEY, основанного на CarBrandId.


Причина: отслеживание изменений

Ошибка может вводить в заблуждение, но если вы заглянете внутрь StateManager своего контекста, вы увидите причину. Поскольку CarBrand, который вы получили от другого Context, это тот, который вы используете, для другого экземпляра Context он выглядит как новый. Вы можете четко увидеть это при отладке со свойством EntityState этой конкретной сущности в этом конкретном контексте БД:

введите описание изображения здесь

В нем говорится, что CarBrand теперь добавляется в базу данных с помощью Id: 1 и что УНИКАЛЬНОЕ ограничение PRIMARY KEY таблицы CarBrand нарушено.

Ошибка говорит о том, что CarBrandId нарушает ограничение, потому что вы принудительно создали новую запись в базе данных через отношение Car-CarBrand, которое поддерживается CarBrandId.

Каким будет состояние CarBrand с Id: 1 в контексте, который вы использовали для запроса базы данных? Без изменений, конечно. Но это единственный Context, кто об этом знает:

введите описание изображения здесь

Если вы хотите углубиться в тему, прочтите о концепции Change Tracking в EF Core здесь: https://docs.microsoft.com/en-us/ef/core/querying/tracking

person mwilczynski    schedule 19.04.2017
comment
что, если мне действительно нужно получить экземпляр CarBrand в другом контексте / транзакции? - person Jan Paolo Go; 19.04.2017
comment
@PaoloGo Вы должны изучить шаблоны репозитория - person David Lee; 19.04.2017
comment
И еще одно дополнение, если вы ДЕЙСТВИТЕЛЬНО хотите использовать отдельные контексты (я понимаю, что, вероятно, вы пытаетесь визуализировать более широкую идею, чем та, которая показана здесь). - person mwilczynski; 19.04.2017