Hubungan satu-ke-banyak EF Core / Sqlite gagal pada Batasan Indeks Unik

Konteksnya adalah setiap Car memiliki CarBrand yang sesuai. Sekarang kelas saya seperti yang ditunjukkan di bawah ini:

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");
    }
}

Berikut ini contoh eksekusi kode saya...

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);
        }
    }
}

Masalahnya adalah saya mendapatkan pengecualian 'UNIQUE constrain failed: CarBrands.CarBrandId' ketika car2 ditambahkan ke database dengan CarBrand honda yang sama.

Apa yang saya harapkan dilakukan adalah selama transaksi ke-2 context.SaveChanges(), ia akan menambahkan car2 dan mengatur hubungannya dengan CarBrand dengan tepat tetapi saya malah mendapatkan pengecualian.

EDIT: Saya benar-benar perlu mendapatkan instance CarBrand saya dalam konteks/transaksi yang berbeda.

        //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 sumber
comment
Tidak yakin apakah ini yang menyebabkan masalah, tapi saya biasanya tidak melihat statis pada operasi CRUD.   -  person David Lee    schedule 19.04.2017
comment
Mengapa Anda harus menggunakan konteks yang berbeda setiap kali?   -  person David Lee    schedule 19.04.2017
comment
katakanlah saya me-restart aplikasi. maka itu akan berada dalam konteks yang berbeda kan?   -  person Jan Paolo Go    schedule 19.04.2017


Jawaban (2)


Masalahnya adalah Add metode kaskade:

Mulai melacak entitas tertentu, dan entitas lain yang dapat dijangkau yang belum dilacak, dalam status Added sehingga entitas tersebut akan dimasukkan ke dalam database saat SaveChanges dipanggil.

Ada banyak cara untuk mencapai tujuan, tetapi yang paling fleksibel (dan menurut saya lebih disukai) adalah mengganti pemanggilan metode Add dengan ChangeTracker.TrackGraph:

Mulai melacak entitas dan entitas apa pun yang dapat dijangkau dengan melintasi properti navigasinya. Traversal bersifat rekursif sehingga properti navigasi dari setiap entitas yang ditemukan juga akan dipindai. Callback yang ditentukan dipanggil untuk setiap entitas yang ditemukan dan harus menyetel Status di mana setiap entitas harus dilacak. Jika tidak ada status yang disetel, entitas tetap tidak terlacak. Metode ini dirancang untuk digunakan dalam skenario terputus di mana entitas diambil menggunakan satu contoh konteks dan kemudian perubahan disimpan menggunakan contoh konteks yang berbeda. Contohnya adalah layanan web di mana satu layanan panggilan mengambil entitas dari database dan panggilan layanan lainnya mempertahankan perubahan apa pun pada entitas. Setiap panggilan layanan menggunakan contoh konteks baru yang dibuang ketika panggilan selesai. Jika entitas ditemukan dan sudah dilacak oleh konteksnya, entitas tersebut tidak diproses (dan properti navigasinya tidak dilintasi).

Jadi, alih-alih context.Cars.Add(car2); Anda dapat menggunakan yang berikut ini (ini cukup umum dan dapat digunakan di hampir semua skenario):

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

Perbaikan cepat:

EntityFramework Core memiliki masalah dengan kode Anda menggunakan Context baru di dalam menggunakan kode lain. Anda mencampurkan status entitas di antara keduanya. Cukup gunakan konteks yang sama untuk mengambil dan membuat. Jika Anda pergi dengan:

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
}

itu seharusnya baik-baik saja.


Jika Anda benar-benar ingin mendapatkan dua konteks

    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();
        }
    }

Anda tidak bergantung pada mekanisme pelacakan perubahan sekarang melainkan pada relasi FOREIGN KEY biasa berdasarkan CarBrandId.


Alasan: Mengubah pelacakan

Kesalahan mungkin menyesatkan tetapi jika Anda melihat ke dalam StateManager konteks Anda, Anda akan melihat alasannya. Karena CarBrand yang Anda ambil dari Context lain adalah yang Anda gunakan, untuk contoh Context yang lain sepertinya baru. Anda dapat melihatnya dengan jelas dalam debug dengan properti EntityState entitas tertentu dalam konteks DB khusus ini:

masukkan deskripsi gambar di sini

Dikatakan bahwa CarBrand sekarang sedang Ditambahkan ke database dengan Id: 1 dan itulah batasan UNIK dari PRIMARY KEY tabel CarBrand yang rusak.

Kesalahan mengatakan CarBrandId melanggar batasan karena Anda memaksa pembuatan catatan baru di database melalui relasi Car-CarBrand yang didukung oleh CarBrandId.

Bagaimana keadaan CarBrand dengan Id: 1 dalam konteks yang Anda gunakan untuk menanyakan database? Tidak berubah tentu saja. Tapi hanya itu Context yang mengetahuinya:

masukkan deskripsi gambar di sini

Jika Anda ingin mendalami topik ini lebih dalam, baca tentang konsep Change Tracking di EF Core di sini: https://docs.microsoft.com/en-us/ef/core/querying/tracking

person mwilczynski    schedule 19.04.2017
comment
bagaimana jika saya benar-benar perlu mendapatkan instance CarBrand dalam konteks/transaksi yang berbeda? - person Jan Paolo Go; 19.04.2017
comment
@PaoloGo Anda harus melihat pola Repositori - person David Lee; 19.04.2017
comment
Dan tambahan lainnya jika Anda BENAR-BENAR ingin menggunakan konteks yang berbeda (saya menyadari bahwa mungkin Anda mencoba memvisualisasikan ide yang lebih luas daripada yang ditunjukkan di sini). - person mwilczynski; 19.04.2017