DbSet.Attach (entitas) vs DbContext.Entry (entitas) .State = EntityState.Modified

115

Ketika saya berada dalam skenario terpisah dan mendapatkan dto dari klien yang saya petakan ke dalam entitas untuk menyimpannya, saya melakukan ini:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

Untuk apa kemudian DbSet.Attach(entity)

atau mengapa saya harus menggunakan metode .Attach ketika EntityState.Modified sudah melampirkan entitas?

Elisabeth
sumber
Lebih baik tambahkan beberapa info versi, ini telah ditanyakan sebelumnya. Saya tidak jelas apakah ini perlu pertanyaan baru.
Henk Holterman

Jawaban:

278

Saat Anda melakukannya context.Entry(entity).State = EntityState.Modified;, Anda tidak hanya melampirkan entitas ke DbContext, Anda juga menandai seluruh entitas sebagai kotor. Ini berarti bahwa ketika Anda melakukannya context.SaveChanges(), EF akan menghasilkan pernyataan pembaruan yang akan memperbarui semua bidang entitas.

Ini tidak selalu diinginkan.

Di sisi lain, DbSet.Attach(entity)melampirkan entitas ke konteks tanpa menandainya sebagai kotor. Itu setara dengan melakukancontext.Entry(entity).State = EntityState.Unchanged;

Saat melampirkan dengan cara ini, kecuali Anda kemudian melanjutkan untuk memperbarui properti di entitas, saat Anda menelepon lagi context.SaveChanges(), EF tidak akan membuat pembaruan database untuk entitas ini.

Bahkan jika Anda berencana untuk membuat pembaruan ke entitas, jika entitas memiliki banyak properti (kolom db) tetapi Anda hanya ingin memperbarui beberapa, Anda mungkin merasa menguntungkan untuk melakukan DbSet.Attach(entity), dan kemudian hanya memperbarui beberapa properti yang perlu diperbarui. Melakukannya dengan cara ini akan menghasilkan pernyataan pembaruan yang lebih efisien dari EF. EF hanya akan memperbarui properti yang Anda modifikasi (berbeda dengan context.Entry(entity).State = EntityState.Modified;yang akan menyebabkan semua properti / kolom diperbarui)

Dokumentasi yang relevan: Tambah / Lampirkan dan Status Entitas .

Contoh kode

Misalkan Anda memiliki entitas berikut:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Jika kode Anda terlihat seperti ini:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

SQL yang dihasilkan akan terlihat seperti ini:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

Perhatikan bagaimana pernyataan pembaruan di atas akan memperbarui semua kolom, terlepas atau apakah Anda benar-benar mengubah nilainya atau tidak.

Sebaliknya, jika kode Anda menggunakan "normal" Lampirkan seperti ini:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();

Kemudian pernyataan pembaruan yang dihasilkan berbeda:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

Seperti yang Anda lihat, pernyataan pembaruan hanya memperbarui nilai yang benar-benar berubah setelah Anda melampirkan entitas ke konteks. Bergantung pada struktur tabel Anda, ini dapat memiliki dampak kinerja yang positif.

Sekarang, opsi mana yang lebih baik untuk Anda sepenuhnya bergantung pada apa yang Anda coba lakukan.

sstan
sumber
1
EF tidak membuat klausa WHERE dengan cara ini. Jika Anda melampirkan entitas yang dibuat dengan baru (yaitu, Entitas baru ()) dan menyetelnya ke modifikasi, Anda harus menyetel semua bidang asli karena kunci optimis. Klausa WHERE yang dihasilkan dalam kueri UPDATE biasanya berisi semua bidang asli (tidak hanya Id) jadi jika Anda tidak melakukannya EF akan mengeluarkan pengecualian konkurensi.
bubi
3
@budi: Terima kasih atas tanggapan Anda. Saya menguji ulang untuk memastikan, dan untuk entitas dasar, ia berperilaku seperti yang saya jelaskan, dengan WHEREklausa yang hanya berisi kunci utama, dan tanpa pemeriksaan konkurensi. Untuk melakukan pemeriksaan konkurensi, saya perlu mengonfigurasi kolom secara eksplisit sebagai token konkurensi atau rowVersion. Dalam hal ini, WHEREklausul hanya akan memiliki kunci utama dan kolom token konkurensi, tidak semua bidang. Jika tes Anda menunjukkan sebaliknya, saya ingin sekali mendengarnya.
sstan
bagaimana saya bisa secara dinamis menemukan properti penyihir diubah?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesdan DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
mungkin sedikit keluar dari topik, tetapi jika saya menggunakan pola repositori, saya harus membuat repositori untuk setiap model karena setiap model memiliki beberapa entitas yang perlu dalam keadaan tidak terlacak saat memasukkan catatan baru dalam db, jadi saya tidak bisa memilikinya repositori umum yang melampirkan entitas ke konteks selama penyisipan. Bagaimana Anda menangani ini dengan sebaik-baiknya?
jayasurya_j
3

Saat Anda menggunakan DbSet.Updatemetode ini, Entity Framework menandai semua properti entitas Anda sebagai EntityState.Modified, jadi lacaklah. Jika Anda ingin mengubah hanya beberapa properti Anda, tidak semuanya, gunakan DbSet.Attach. Metode ini membuat semua properti Anda EntityState.Unchanged, jadi Anda harus membuat properti yang ingin Anda perbarui EntityState.Modified. Jadi, saat aplikasi mencapai DbContext.SaveChanges, itu hanya akan mengoperasikan properti yang dimodifikasi.

Orhun
sumber
0

Hanya sebagai tambahan (pada jawaban yang ditandai) ada perbedaan penting antara context.Entry(entity).State = EntityState.Unchangeddan context.Attach(entity)(dalam EF Core):

Saya melakukan beberapa tes untuk lebih memahaminya sendiri (oleh karena itu ini juga termasuk beberapa pengujian referensi umum), jadi ini adalah skenario pengujian saya:

  • Saya menggunakan EF Core 3.1.3
  • Saya dulu QueryTrackingBehavior.NoTracking
  • Saya hanya menggunakan atribut untuk pemetaan (lihat di bawah)
  • Saya menggunakan konteks yang berbeda untuk mendapatkan pesanan dan memperbarui pesanan
  • Saya menghapus seluruh db untuk setiap tes

Ini adalah modelnya:

public class Order
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public string ShippingAddress { get; set; }
    public DateTime? OrderDate { get; set; }
    public List<OrderPos> OrderPositions { get; set; }
    [ForeignKey("OrderedByUserId")]
    public User OrderedByUser { get; set; }
    public int? OrderedByUserId { get; set; }
}

public class OrderPos
{
    public int Id { get; set; }
    public string ArticleNo { get; set; }
    public int Quantity { get; set; }
    [ForeignKey("OrderId")]
    public Order Order { get; set; }
    public int? OrderId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Ini adalah data uji (asli) dalam database: masukkan deskripsi gambar di sini

Untuk mendapatkan pesanan:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

Sekarang tesnya:

Pembaruan Sederhana dengan EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Pembaruan Sederhana dengan Lampirkan :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Perbarui dengan mengubah Id-Anak dengan EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Perbarui dengan mengubah Id Anak dengan Lampirkan :

db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)

Catatan: Ini melempar Exception, tidak peduli apakah Id diubah atau disetel ke nilai asli, sepertinya status Id disetel ke "diubah" dan ini tidak diperbolehkan (karena itu kunci utama)

Perbarui dengan mengubah ID Anak sebagai yang baru (tidak ada perbedaan antara EntityState dan Attach):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3

Catatan: Lihat perbedaan Update dengan EntityState tanpa new (di atas). Kali ini Nama akan diperbarui, karena contoh Pengguna baru.

Perbarui dengan mengubah Reference-Id dengan EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1

Perbarui dengan mengubah Referensi-Id dengan Lampirkan :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Catatan: Referensi akan diubah menjadi Pengguna 3, tetapi juga pengguna 1 akan diperbarui, saya kira ini karena order.OrderedByUser.Idtidak berubah (masih 1).

Kesimpulan Dengan EntityState Anda memiliki kontrol lebih, tetapi Anda harus memperbarui sub-properti (tingkat kedua) sendiri. Dengan Lampirkan Anda dapat memperbarui semuanya (saya kira dengan semua tingkat properti), tetapi Anda harus tetap memperhatikan referensi. Sebagai contoh: Jika User (OrderedByUser) akan menjadi dropDown, mengubah nilai melalui dropDown mungkin menimpa seluruh objek User. Dalam kasus ini, dropDown-Value asli akan ditimpa, bukan referensi.

Bagi saya kasus terbaik adalah mengatur objek seperti OrderedByUser ke null dan hanya mengatur order.OrderedByUserId ke nilai baru, jika saya hanya ingin mengubah referensi (tidak peduli apakah EntityState atau Attach).

Semoga ini bisa membantu, saya tahu teksnya banyak: D

StewieG
sumber