InvalidOperationException yang tidak terduga saat mencoba mengubah hubungan melalui nilai default properti

10

Dalam kode contoh di bawah ini saya mendapatkan pengecualian berikut saat melakukan db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'Instance dari tipe entitas' B 'tidak dapat dilacak karena instance lain dengan nilai kunci' {Id: 0} 'sudah dilacak. Saat melampirkan entitas yang ada, pastikan bahwa hanya satu instance entitas dengan nilai kunci tertentu yang dilampirkan.

Mengapa itu tidak menambah dan bukannya melampirkan contoh B?

Anehnya dokumentasi untuk IsModifiedtidak menentukan InvalidOperationExceptionsebagai pengecualian yang mungkin. Dokumentasi atau bug tidak valid?

Saya tahu kode ini aneh, tetapi saya menulisnya hanya untuk memahami bagaimana ef core bekerja dalam beberapa kasus egde yang aneh. Yang saya inginkan adalah penjelasan, bukan pekerjaan.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}
Supremum
sumber
Bagaimana hubungan A dan B? artinya apa hubungan properti?
sam

Jawaban:

8

Alasan kesalahan dalam kode yang disediakan adalah sebagai berikut.

Ketika Anda membuat entitas Adari database, propertinya Sdiinisialisasi dengan koleksi yang berisi dua catatan baru B. Iddari masing-masing Bentitas baru ini sama dengan 0.

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

Setelah mengeksekusi baris var a = db.Set<A>().Single()pengumpulan kode Sentitas Atidak mengandung Bentitas dari database, karena DbContext Dbtidak menggunakan lazy loading dan tidak ada pemuatan eksplisit koleksi S. Entitas Ahanya berisi Bentitas baru yang dibuat selama inisialisasi koleksi S.

Saat Anda menelepon IsModifed = trueuntuk Skerangka kerja entitas kumpulan mencoba menambahkan dua entites baru Bke dalam pelacakan perubahan. Tetapi gagal karena kedua Bentitas baru memiliki hal yang sama Id = 0:

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

Anda bisa melihat dari jejak tumpukan yang dicoba ditambahkan kerangka kerja Bentitas ke IdentityMap:

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

Dan pesan kesalahan juga memberitahu bahwa ia tidak dapat melacak Bentitas dengan Id = 0karena Bentitas lain dengan yang sama Idsudah dilacak.


Cara mengatasi masalah ini.

Untuk mengatasi masalah ini, Anda harus menghapus kode yang membuat Bentitas saat menginisialisasi Skoleksi:

public ICollection<B> S { get; set; } = new List<B>();

Sebaliknya Anda harus mengisi Skoleksi di tempat di mana Adibuat. Sebagai contoh:

db.Add(new A {S = {new B(), new B()}});

Jika Anda tidak menggunakan lazy loading, Anda harus secara eksplisit memuat Skoleksi untuk menambahkan itemnya ke dalam pelacakan perubahan:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Mengapa itu tidak menambah dan bukannya melampirkan contoh B?

Singkatnya , mereka dilampirkan untuk ditambahkan karena mereka memiliki Detachedkeadaan.

Setelah mengeksekusi baris kode

var a = db.Set<A>().Single();

instance entitas yang dibuat Bmemiliki status Detached. Itu dapat diverifikasi menggunakan kode selanjutnya:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

Lalu ketika Anda mengatur

db.Entry(a).Collection(x => x.S).IsModified = true;

EF mencoba menambahkan Bentitas untuk mengubah pelacakan. Dari kode sumber EFCore, Anda dapat melihat bahwa ini mengarahkan kami ke metode InternalEntityEntry.SetPropertyModified dengan nilai argumen berikut:

  • property- salah satu Bentitas kami ,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Metode ini dengan argumen seperti itu mengubah keadaan dari Detached Bentit ke Modified, dan kemudian mencoba untuk memulai pelacakan untuk mereka (lihat baris 490 - 506). Karena Bentitas sekarang memiliki status Modifiedini menyebabkan mereka harus dilampirkan (tidak ditambahkan).

Iliar Turdushev
sumber
Di mana jawaban untuk "Mengapa tidak ditambahkan alih-alih melampirkan instance B?" Anda mengatakan "gagal karena kedua entitas B baru memiliki Id = 0" yang sama. Saya pikir itu salah karena ef core menghemat keduanya dengan 1 dan 2 id. Saya kira itu bukan jawaban yang benar untuk pertanyaan ini
DIlshod K
@DIlshod K Terima kasih atas komentarnya. Di bagian "Cara mengatasi masalah ini" saya menulis bahwa koleksi Sharus dimuat secara eksplisit, karena kode yang disediakan tidak menggunakan lazy loading. Tentu saja, EF menyimpan Bentitas yang sebelumnya dibuat dalam database. Tetapi baris kode A a = db.Set<A>().Single()hanya memuat entitas Atanpa entitas dalam koleksi S. Untuk memuat koleksi, Spemuatan yang cepat harus digunakan. Saya akan mengubah asnwer saya untuk secara eksplisit memasukkan jawaban untuk pertanyaan "Mengapa tidak menambahkan bukannya melampirkan contoh B?".
Iliar Turdushev