Bagaimana cara menghentikan Entity Framework mencoba menyimpan / menyisipkan objek turunan?

102

Ketika saya menyimpan entitas dengan kerangka entitas, saya secara alami berasumsi itu hanya akan mencoba untuk menyimpan entitas yang ditentukan. Namun, entitas juga mencoba menyelamatkan entitas anak entitas tersebut. Ini menyebabkan segala macam masalah integritas. Bagaimana cara memaksa EF untuk hanya menyimpan entitas yang ingin saya simpan dan karena itu mengabaikan semua objek turunan?

Jika saya secara manual menyetel properti ke nol, saya mendapatkan pesan kesalahan "Operasi gagal: Hubungan tidak dapat diubah karena satu atau lebih properti kunci asing tidak dapat dibatalkan." Ini sangat kontraproduktif karena saya menetapkan objek anak ke nol secara khusus sehingga EF akan membiarkannya.

Mengapa saya tidak ingin menyimpan / menyisipkan objek anak?

Karena ini sedang dibahas bolak-balik di komentar, saya akan memberikan beberapa pembenaran mengapa saya ingin objek anak saya dibiarkan sendiri.

Dalam aplikasi yang saya buat, model objek EF tidak dimuat dari database tetapi digunakan sebagai objek data yang saya isi saat mengurai file datar. Dalam kasus objek anak, banyak di antaranya merujuk ke tabel pemeta yang mendefinisikan berbagai properti dari tabel induk. Misalnya, lokasi geografis dari entitas utama.

Karena saya telah mengisi objek ini sendiri, EF mengasumsikan ini adalah objek baru dan perlu disisipkan bersama dengan objek induk. Namun, definisi ini sudah ada dan saya tidak ingin membuat duplikat di database. Saya hanya menggunakan objek EF untuk melakukan pencarian dan mengisi kunci asing di entitas tabel utama saya.

Bahkan dengan objek anak yang merupakan data nyata, saya perlu menyimpan induknya terlebih dahulu dan mendapatkan kunci utama atau EF sepertinya akan membuat kekacauan. Semoga ini memberi penjelasan.

Tandai Micallef
sumber
Sejauh yang saya tahu Anda harus membatalkan objek anak.
Johan
Hai Johan. Tidak berhasil. Ini melempar kesalahan jika saya membatalkan koleksi. Bergantung pada bagaimana saya melakukannya, itu mengeluh tentang kunci menjadi nol atau koleksi saya telah dimodifikasi. Jelas, hal-hal itu benar, tetapi saya melakukannya dengan sengaja sehingga akan meninggalkan objek yang tidak seharusnya disentuh.
Mark Micallef
Euforia, itu sama sekali tidak membantu.
Mark Micallef
@Euphoric Meskipun tidak mengubah objek anak, EF masih mencoba untuk memasukkannya secara default dan tidak mengabaikan atau memperbaruinya.
Johan
Apa yang benar-benar mengganggu saya adalah bahwa jika saya berusaha keras untuk benar-benar membatalkan objek-objek itu, ia kemudian mengeluh daripada menyadari bahwa saya ingin membiarkan mereka begitu saja. Karena objek anak tersebut semuanya opsional (nullable dalam database), apakah ada cara untuk memaksa EF lupa bahwa saya memiliki objek tersebut? yaitu membersihkan konteksnya atau cache?
Mark Micallef

Jawaban:

56

Sejauh yang saya tahu, Anda memiliki dua opsi.

Pilihan 1)

Hapus semua objek anak, ini akan memastikan EF tidak menambahkan apapun. Ini juga tidak akan menghapus apapun dari database Anda.

Pilihan 2)

Tetapkan objek anak sebagai terlepas dari konteks menggunakan kode berikut

 context.Entry(yourObject).State = EntityState.Detached

Perhatikan bahwa Anda tidak dapat melepaskan List/ Collection. Anda harus mengulang daftar Anda dan melepaskan setiap item dalam daftar Anda seperti itu

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Johan
sumber
Hai Johan, saya mencoba melepaskan salah satu koleksi dan menimbulkan kesalahan berikut: Jenis entitas HashSet`1 bukan bagian dari model untuk konteks saat ini.
Mark Micallef
@ Marky Jangan lepaskan koleksinya. Anda harus mengulang koleksi dan melepaskan objek untuk objek (saya akan memperbarui jawaban saya sekarang).
Johan
11
Sayangnya, bahkan dengan daftar loop untuk melepaskan semuanya, EF tampaknya masih mencoba untuk memasukkan ke dalam beberapa tabel terkait. Pada titik ini, saya siap untuk menghilangkan EF dan beralih kembali ke SQL yang setidaknya berperilaku baik. Sakit sekali.
Mark Micallef
2
Dapatkah saya menggunakan context.Entry (yourObject). Status bahkan sebelum menambahkannya?
Thomas Klammer
1
@ mirind4 Saya tidak menggunakan negara. Jika saya memasukkan sebuah objek, saya pastikan semua anaknya null. Pada pembaruan saya mendapatkan objek pertama tanpa anaknya.
Thomas Klammer
41

Singkat cerita: Gunakan kunci Asing dan itu akan menghemat hari Anda.

Misalnya Anda memiliki entitas Sekolah dan entitas Kota , dan ini adalah hubungan banyak ke satu di mana Kota memiliki banyak Sekolah dan Sekolah milik Kota. Dan asumsikan Cities sudah ada di tabel pemeta sehingga Anda TIDAK ingin kota tersebut disisipkan lagi saat memasukkan sekolah baru.

Awalnya Anda mungkin mendefinisikan entitas Anda seperti ini:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

Dan Anda mungkin melakukan penyisipan Sekolah seperti ini (asumsikan Anda sudah memiliki properti Kota yang ditetapkan ke item baru ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

Pendekatan di atas dapat bekerja dengan sempurna dalam kasus ini, namun, saya lebih suka pendekatan Kunci Asing yang menurut saya lebih jelas dan fleksibel. Lihat solusi terbaru di bawah ini:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Dengan cara ini, Anda secara eksplisit mendefinisikan bahwa Sekolah memiliki kunci asing City_Id dan mengacu pada entitas Kota . Jadi, jika menyangkut penyisipan Sekolah , Anda dapat melakukan:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

Dalam kasus ini, Anda secara eksplisit menentukan City_Id dari record baru dan menghapus City dari grafik sehingga EF tidak akan repot-repot menambahkannya ke konteks bersama dengan Sekolah .

Meskipun pada kesan pertama pendekatan kunci Asing tampak lebih rumit, tetapi percayalah, mentalitas ini akan menghemat banyak waktu ketika harus memasukkan hubungan banyak-ke-banyak (bayangkan Anda memiliki hubungan Sekolah dan Siswa, dan Siswa memiliki properti Kota) dan seterusnya.

Semoga ini bisa membantu Anda.

lenglei
sumber
Jawaban yang bagus, banyak membantu saya! Tetapi bukankah lebih baik untuk menyatakan nilai minimum 1 untuk City_Id menggunakan [Range(1, int.MaxValue)]atribut?
Dan Rayson
Seperti mimpi!! Terima kasih banyak!
CJH
Ini akan menghapus nilai dari objek Sekolah di pemanggil. Apakah SaveChanges memuat ulang nilai properti navigasi nol? Jika tidak, pemanggil harus memuat ulang objek City setelah memanggil metode Insert () jika membutuhkan info itu. Ini adalah pola yang sering saya gunakan, tetapi saya masih terbuka untuk pola yang lebih baik jika ada yang memiliki pola yang bagus.
Miring
1
Ini sangat membantu saya. EntityState.Unchaged persis seperti yang saya butuhkan untuk menetapkan objek yang mewakili kunci asing tabel pencarian dalam grafik objek besar yang saya simpan sebagai satu transaksi. EF Core mengirimkan pesan kesalahan yang kurang dari intuitif IMO. Saya meng-cache tabel pencarian saya yang jarang berubah karena alasan kinerja. Asumsi Anda adalah, karena PK dari objek tabel pencarian sama dengan apa yang sudah ada di database yang tahu itu tidak berubah dan hanya menetapkan FK ke item yang ada. Alih-alih mencoba memasukkan objek tabel pencarian sebagai baru.
tnk479
Tidak ada kombinasi dari pembatalan atau pengaturan status menjadi tidak berubah yang bekerja untuk saya. EF bersikeras perlu memasukkan catatan anak baru ketika saya hanya ingin memperbarui bidang skalar di induknya.
BobRz
21

Jika Anda hanya ingin menyimpan perubahan pada orangtua objek dan menghindari menyimpan perubahan ke salah satu nya anak objek, maka mengapa tidak hanya melakukan hal berikut:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

Baris pertama menempelkan objek induk dan seluruh grafik objek turunan dependennya ke konteks dalam Unchangedstatus.

Baris kedua mengubah status untuk objek induk saja, membiarkan turunannya dalam Unchangedstatus.

Perhatikan bahwa saya menggunakan konteks yang baru dibuat, jadi ini menghindari penyimpanan perubahan lain ke database.

keykey76
sumber
2
Jawaban ini memiliki keuntungan bahwa jika seseorang datang kemudian dan menambahkan objek turunan, itu tidak akan merusak kode yang ada. Ini adalah solusi "keikutsertaan" di mana yang lain meminta Anda untuk mengecualikan objek turunan secara eksplisit.
Jim
Ini tidak lagi berfungsi pada inti. "Lampirkan: Melampirkan setiap entitas yang dapat dijangkau, kecuali jika entitas yang dapat dijangkau memiliki kunci yang dihasilkan toko dan tidak ada nilai kunci yang ditetapkan; ini akan ditandai sebagai ditambahkan." Jika anak-anak baru juga mereka akan ditambahkan.
mmix
14

Salah satu solusi yang disarankan adalah menetapkan properti navigasi dari konteks database yang sama. Dalam solusi ini, properti navigasi yang ditetapkan dari luar konteks database akan diganti. Silakan lihat contoh berikut untuk ilustrasi.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Menyimpan ke database:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft mendaftarkan ini sebagai fitur, namun menurut saya ini mengganggu. Jika objek departemen yang terkait dengan objek perusahaan memiliki Id yang sudah ada di database, mengapa EF tidak mengaitkan objek perusahaan dengan objek database saja? Mengapa kita perlu mengurus pergaulan sendiri? Menjaga properti navigasi selama menambahkan objek baru adalah sesuatu seperti memindahkan operasi database dari SQL ke C #, merepotkan pengembang.

Bikash Bishwokarma
sumber
Saya setuju 100% konyol bahwa EF mencoba membuat rekor baru ketika ID diberikan. Jika ada cara untuk mengisi opsi daftar pilih dengan objek sebenarnya yang akan kami jalankan.
T3.0
11

Pertama, Anda perlu tahu bahwa ada dua cara untuk memperbarui entitas di EF.

  • Objek terlampir

Saat Anda mengubah hubungan objek yang dilampirkan ke konteks objek dengan menggunakan salah satu metode yang dijelaskan di atas, Entity Framework perlu menjaga kunci asing, referensi, dan koleksi tetap sinkron.

  • Objek yang terputus

Jika Anda bekerja dengan objek yang terputus, Anda harus mengelola sinkronisasi secara manual.

Dalam aplikasi yang saya buat, model objek EF tidak dimuat dari database tetapi digunakan sebagai objek data yang saya isi saat mengurai file datar.

Itu berarti Anda bekerja dengan objek terputus, tetapi tidak jelas apakah Anda menggunakan asosiasi independen atau asosiasi kunci asing .

  • Menambahkan

    Saat menambahkan entitas baru dengan objek anak yang sudah ada (objek yang ada di database), jika objek anak tidak dilacak oleh EF, objek anak akan disisipkan kembali. Kecuali jika Anda memasang objek anak secara manual terlebih dahulu.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Memperbarui

    Anda cukup menandai entitas sebagai diubah, kemudian semua properti skalar akan diperbarui dan properti navigasi akan diabaikan begitu saja.

      db.Entity(entity).State = EntityState.Modified;

Grafik Diff

Jika Anda ingin menyederhanakan kode saat bekerja dengan objek terputus, Anda dapat mencoba untuk membuat grafik di perpustakaan diff .

Berikut adalah pendahuluan, Memperkenalkan GraphDiff untuk Kode Kerangka Entitas Pertama - Mengizinkan pembaruan otomatis dari grafik entitas yang terpisah .

Kode sampel

  • Masukkan entitas jika tidak ada, jika tidak perbarui.

      db.UpdateGraph(entity);
  • Sisipkan entitas jika tidak ada, jika tidak, perbarui DAN sisipkan objek anak jika tidak ada, jika tidak perbarui.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
sumber
3

Cara terbaik untuk melakukannya adalah dengan menimpa fungsi SaveChanges di konteks data Anda.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Wouter Schut
sumber
2

Saya mendapat masalah yang sama ketika saya mencoba menyimpan profil, saya sudah salam tabel dan baru membuat profil. Ketika saya memasukkan profil, itu juga dimasukkan ke dalam salam. Jadi saya mencoba seperti ini sebelum savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
sumber
1

Ini berhasil untuk saya:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Klaus
sumber
0

Apa yang telah kita lakukan adalah sebelum menambahkan induk ke dbset, memutuskan koleksi anak dari induk, memastikan untuk mendorong koleksi yang ada ke variabel lain untuk memungkinkan bekerja dengannya nanti, dan kemudian mengganti koleksi anak saat ini dengan koleksi kosong baru. Menyetel koleksi anak ke nol / tidak ada yang gagal bagi kami. Setelah melakukan itu, tambahkan induk ke dbset. Dengan cara ini anak-anak tidak ditambahkan sampai Anda menginginkannya.

Shaggie
sumber
0

Saya tahu itu posting lama namun jika Anda menggunakan pendekatan kode-pertama Anda dapat mencapai hasil yang diinginkan dengan menggunakan kode berikut di file pemetaan Anda.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Ini pada dasarnya akan memberitahu EF untuk mengecualikan properti "ChildObjectOrCollection" dari model sehingga tidak dipetakan ke database.

Syed Denmark
sumber
Dalam konteks apa "Abaikan" digunakan? Tampaknya tidak ada dalam konteks yang dianggap.
T3.0
0

Saya memiliki tantangan serupa menggunakan Entity Framework Core 3.1.0, logika repo saya cukup umum.

Ini berhasil untuk saya:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Harap diperhatikan "ParentEntityId" adalah nama kolom kunci asing pada entitas anak. Saya menambahkan baris kode yang disebutkan di atas pada metode ini:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
sumber