ความสัมพันธ์แบบหนึ่งต่อกลุ่มของ 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 เดียวกัน

สิ่งที่ฉันคาดหวังว่าจะทำคือระหว่างธุรกรรมที่ 2 ของ 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 ด้วย ChangeTracker.TrackGraph วิธีการ:

เริ่มต้นการติดตามเอนทิตีและเอนทิตีใดๆ ที่สามารถเข้าถึงได้โดยการสำรวจคุณสมบัติการนำทาง การข้ามผ่านเป็นแบบเรียกซ้ำ ดังนั้นคุณสมบัติการนำทางของเอนทิตีที่ค้นพบใดๆ จะถูกสแกนด้วย การเรียกกลับที่ระบุถูกเรียกสำหรับเอนทิตีที่ค้นพบแต่ละรายการ และต้องตั้งค่าสถานะที่ควรติดตามเอนทิตีแต่ละรายการ หากไม่มีการตั้งค่าสถานะ เอนทิตีจะยังคงไม่ถูกติดตาม วิธีการนี้ได้รับการออกแบบเพื่อใช้ในสถานการณ์ที่ไม่ได้เชื่อมต่อ โดยที่เอนทิตีถูกดึงข้อมูลโดยใช้อินสแตนซ์หนึ่งของบริบท จากนั้นการเปลี่ยนแปลงจะถูกบันทึกโดยใช้อินสแตนซ์ที่แตกต่างกันของบริบท ตัวอย่างนี้คือบริการเว็บที่บริการหนึ่ง การเรียกดึงเอนทิตีจากฐานข้อมูลและการเรียกบริการอื่นยังคงมีการเปลี่ยนแปลงใด ๆ กับเอนทิตี การเรียกใช้บริการแต่ละครั้งจะใช้อินสแตนซ์ใหม่ของบริบทที่จะถูกจัดการเมื่อการโทรเสร็จสมบูรณ์ หากค้นพบเอนทิตีที่ถูกติดตามโดยบริบทแล้ว เอนทิตีนั้นจะไม่ได้รับการประมวลผล (และคุณสมบัติการนำทางจะไม่ถูกสำรวจ)

ดังนั้นแทนที่จะใช้ 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 มีปัญหากับโค้ดของคุณโดยใช้ new 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 ของเอนทิตีนั้น ๆ ในบริบทของ DB เฉพาะนี้:

ป้อนคำอธิบายรูปภาพที่นี่

มันบอกว่าขณะนี้ CarBrand กำลัง เพิ่ม ลงในฐานข้อมูลด้วย Id: 1 และนั่นคือข้อจำกัด UNIQUE ของ 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