Fitur Pelacakan Perubahan Tambahan
Dokumen ini mencakup fitur dan skenario lain-lain yang melibatkan pelacakan perubahan.
Tip
Dokumen ini mengasumsikan bahwa status entitas dan dasar-dasar pelacakan perubahan EF Core dipahami. Lihat Pelacakan Perubahan di EF Core untuk informasi selengkapnya tentang topik ini.
Tip
Anda dapat menjalankan dan men-debug ke semua kode dalam dokumen ini dengan mengunduh kode sampel dari GitHub.
Add
versus AddAsync
Entity Framework Core (EF Core) menyediakan metode asinkron setiap kali menggunakan metode tersebut dapat mengakibatkan interaksi database. Metode sinkron juga disediakan untuk menghindari overhead saat menggunakan database yang tidak mendukung akses asinkron performa tinggi.
DbContext.Add dan DbSet<TEntity>.Add biasanya tidak mengakses database, karena metode ini secara inheren hanya mulai melacak entitas. Namun, beberapa bentuk pembuatan nilai dapat mengakses database untuk menghasilkan nilai kunci. Satu-satunya generator nilai yang melakukan ini dan dikirim dengan EF Core adalah HiLoValueGenerator<TValue>. Menggunakan generator ini jarang; tidak pernah dikonfigurasi secara default. Ini berarti bahwa sebagian besar aplikasi harus menggunakan Add
, dan bukan AddAsync
.
Metode serupa lainnya seperti Update
, Attach
, dan Remove
tidak memiliki kelebihan asinkron karena tidak pernah menghasilkan nilai kunci baru, dan karenanya tidak perlu mengakses database.
AddRange
, UpdateRange
, AttachRange
, dan RemoveRange
DbSet<TEntity>dan DbContext menyediakan versi alternatif dari Add
, , Update
Attach
, dan Remove
yang menerima beberapa instans dalam satu panggilan. Metode ini masing-masing adalah AddRange, UpdateRange, AttachRange, dan RemoveRange .
Metode ini disediakan sebagai kenyamanan. Menggunakan metode "rentang" memiliki fungsionalitas yang sama dengan beberapa panggilan ke metode non-rentang yang setara. Tidak ada perbedaan performa yang signifikan antara kedua pendekatan tersebut.
Catatan
Ini berbeda dari EF6, di mana AddRange
dan Add
keduanya secara otomatis memanggil DetectChanges
, tetapi memanggil Add
beberapa kali menyebabkan DetectChanges dipanggil beberapa kali alih-alih sekali. Ini membuat AddRange
lebih efisien di EF6. Di EF Core, tidak satu pun dari metode ini secara otomatis memanggil DetectChanges
.
Metode DbContext versus DbSet
Banyak metode, termasuk Add
, , Update
Attach
, dan Remove
, memiliki implementasi pada dan DbSet<TEntity>DbContext. Metode ini memiliki perilaku yang sama persis untuk jenis entitas normal. Ini karena jenis CLR entitas dipetakan ke satu dan hanya satu jenis entitas dalam model EF Core. Oleh karena itu, jenis CLR sepenuhnya menentukan di mana entitas cocok dalam model, sehingga DbSet yang akan digunakan dapat ditentukan secara implisit.
Pengecualian untuk aturan ini adalah saat menggunakan jenis entitas jenis bersama, yang terutama digunakan untuk entitas gabungan banyak-ke-banyak. Saat menggunakan jenis entitas jenis bersama, DbSet harus terlebih dahulu dibuat untuk jenis model EF Core yang sedang digunakan. Metode seperti Add
, Update
, Attach
, dan Remove
kemudian dapat digunakan pada DbSet tanpa ambiguitas apa pun tentang jenis model EF Core yang digunakan.
Jenis entitas jenis bersama digunakan secara default untuk entitas gabungan dalam hubungan banyak ke banyak. Jenis entitas jenis bersama juga dapat dikonfigurasi secara eksplisit untuk digunakan dalam hubungan banyak ke banyak. Misalnya, kode di bawah ini mengonfigurasi Dictionary<string, int>
sebagai jenis entitas gabungan:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.SharedTypeEntity<Dictionary<string, int>>(
"PostTag",
b =>
{
b.IndexerProperty<int>("TagId");
b.IndexerProperty<int>("PostId");
});
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, int>>(
"PostTag",
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany());
}
Mengubah Kunci Asing dan Navigasi menunjukkan cara mengaitkan dua entitas dengan melacak instans entitas gabungan baru. Kode di bawah ini melakukan ini untuk Dictionary<string, int>
jenis entitas jenis bersama yang digunakan untuk entitas gabungan:
using var context = new BlogsContext();
var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);
var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Perhatikan bahwa DbContext.Set<TEntity>(String) digunakan untuk membuat DbSet untuk PostTag
jenis entitas. DbSet ini kemudian dapat digunakan untuk memanggil Add
dengan instans entitas gabungan baru.
Penting
Jenis CLR yang digunakan untuk jenis entitas gabungan menurut konvensi dapat berubah dalam rilis mendatang untuk meningkatkan performa. Jangan bergantung pada jenis entitas gabungan tertentu kecuali telah dikonfigurasi secara eksplisit seperti yang dilakukan untuk Dictionary<string, int>
dalam kode di atas.
Properti versus akses bidang
Akses ke properti entitas menggunakan bidang backing properti secara default. Ini efisien dan menghindari pemicu efek samping dari pemanggil properti getter dan setter. Misalnya, ini adalah bagaimana pemuatan malas dapat menghindari pemicu perulangan tak terbatas. Lihat Bidang Backing untuk informasi selengkapnya tentang mengonfigurasi bidang dukungan dalam model.
Terkadang mungkin diinginkan bagi EF Core untuk menghasilkan efek samping ketika memodifikasi nilai properti. Misalnya, ketika pengikatan data ke entitas, mengatur properti dapat menghasilkan pemberitahuan ke U.I. yang tidak terjadi saat mengatur bidang secara langsung. Ini dapat dicapai dengan mengubah PropertyAccessMode untuk:
- Semua jenis entitas dalam model menggunakan ModelBuilder.UsePropertyAccessMode
- Semua properti dan navigasi jenis entitas tertentu menggunakan EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Properti tertentu menggunakan PropertyBuilder.UsePropertyAccessMode
- Navigasi tertentu menggunakan NavigationBuilder.UsePropertyAccessMode
Mode Field
akses properti dan PreferField
akan menyebabkan EF Core mengakses nilai properti melalui bidang dukungannya. Demikian juga, Property
dan PreferProperty
akan menyebabkan EF Core mengakses nilai properti melalui getter dan setter-nya.
Jika Field
atau Property
digunakan dan EF Core tidak dapat mengakses nilai melalui bidang atau properti getter/setter masing-masing, maka EF Core akan memberikan pengecualian. Ini memastikan EF Core selalu menggunakan akses bidang/properti saat Anda merasa demikian.
Di sisi lain, PreferField
mode dan PreferProperty
akan kembali menggunakan properti atau bidang pencadangan masing-masing jika tidak memungkinkan untuk menggunakan akses yang disukai. PreferField
adalah default. Ini berarti EF Core akan menggunakan bidang kapan pun dapat, tetapi tidak akan gagal jika properti harus diakses melalui getter atau setter-nya sebagai gantinya.
FieldDuringConstruction
dan PreferFieldDuringConstruction
konfigurasikan EF Core untuk menggunakan bidang backing hanya saat membuat instans entitas. Ini memungkinkan kueri dijalankan tanpa efek samping getter dan setter, sementara nantinya perubahan properti oleh EF Core akan menyebabkan efek samping ini.
Mode akses properti yang berbeda dirangkum dalam tabel berikut:
PropertyAccessMode | Preferensi | Preferensi membuat entitas | Fallback | Fallback membuat entitas |
---|---|---|---|---|
Field |
Bidang | Bidang | Melemparkan | Melemparkan |
Property |
Properti | Properti | Melemparkan | Melemparkan |
PreferField |
Bidang | Bidang | Properti | Properti |
PreferProperty |
Properti | Properti | Bidang | Bidang |
FieldDuringConstruction |
Properti | Bidang | Bidang | Melemparkan |
PreferFieldDuringConstruction |
Properti | Bidang | Bidang | Properti |
Nilai sementara
EF Core membuat nilai kunci sementara saat melacak entitas baru yang akan memiliki nilai kunci nyata yang dihasilkan oleh database saat SaveChanges dipanggil. Lihat Pelacakan Perubahan di EF Core untuk gambaran umum tentang bagaimana nilai sementara ini digunakan.
Mengakses nilai sementara
Nilai sementara disimpan di pelacak perubahan dan tidak diatur ke instans entitas secara langsung. Namun, nilai sementara ini diekspos saat menggunakan berbagai mekanisme untuk Mengakses Entitas terlacak. Misalnya, kode berikut mengakses nilai sementara menggunakan EntityEntry.CurrentValues:
using var context = new BlogsContext();
var blog = new Blog { Name = ".NET Blog" };
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
Output dari kode ini adalah:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary dapat digunakan untuk memeriksa nilai sementara.
Memanipulasi nilai sementara
Terkadang berguna untuk bekerja secara eksplisit dengan nilai sementara. Misalnya, kumpulan entitas baru mungkin dibuat pada klien web lalu diserialisasikan kembali ke server. Nilai kunci asing adalah salah satu cara untuk menyiapkan hubungan antara entitas ini. Kode berikut menggunakan pendekatan ini untuk mengaitkan grafik entitas baru dengan kunci asing, sambil tetap memungkinkan nilai kunci nyata dihasilkan saat SaveChanges dipanggil.
var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };
var posts = new List<Post>
{
new Post
{
Id = -1,
BlogId = -1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = -2,
BlogId = -2,
Title = "Disassembly improvements for optimized managed debugging",
Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
}
};
using var context = new BlogsContext();
foreach (var blog in blogs)
{
context.Add(blog).Property(e => e.Id).IsTemporary = true;
}
foreach (var post in posts)
{
context.Add(post).Property(e => e.Id).IsTemporary = true;
}
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Perhatikan bahwa:
- Angka negatif digunakan sebagai nilai kunci sementara; ini tidak diperlukan, tetapi merupakan konvensi umum untuk mencegah bentrokan kunci.
- Properti
Post.BlogId
FK diberi nilai negatif yang sama dengan PK blog terkait. - Nilai PK ditandai sebagai sementara dengan mengatur IsTemporary setelah setiap entitas dilacak. Ini diperlukan karena nilai kunci apa pun yang disediakan oleh aplikasi diasumsikan sebagai nilai kunci nyata.
Melihat tampilan debug pelacak perubahan sebelum memanggil SaveChanges menunjukkan bahwa nilai PK ditandai sebagai sementara dan postingan dikaitkan dengan blog yang benar, termasuk perbaikan navigasi:
Blog {Id: -2} Added
Id: -2 PK Temporary
Name: 'Visual Studio Blog'
Posts: [{Id: -2}]
Blog {Id: -1} Added
Id: -1 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -1}]
Post {Id: -2} Added
Id: -2 PK Temporary
BlogId: -2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: -2}
Tags: []
Post {Id: -1} Added
Id: -1 PK Temporary
BlogId: -1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -1}
Setelah memanggil SaveChanges, nilai sementara ini telah digantikan oleh nilai nyata yang dihasilkan oleh database:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Posts: [{Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Bekerja dengan nilai default
EF Core memungkinkan properti untuk mendapatkan nilai defaultnya dari database saat SaveChanges dipanggil. Seperti nilai kunci yang dihasilkan, EF Core hanya akan menggunakan default dari database jika tidak ada nilai yang ditetapkan secara eksplisit. Misalnya, pertimbangkan jenis entitas berikut:
public class Token
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ValidFrom { get; set; }
}
Properti ValidFrom
dikonfigurasi untuk mendapatkan nilai default dari database:
modelBuilder
.Entity<Token>()
.Property(e => e.ValidFrom)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
Saat menyisipkan entitas jenis ini, EF Core akan membiarkan database menghasilkan nilai kecuali nilai eksplisit telah ditetapkan sebagai gantinya. Contohnya:
using var context = new BlogsContext();
context.AddRange(
new Token { Name = "A" },
new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Melihat tampilan debug pelacak perubahan menunjukkan bahwa token pertama telah ValidFrom
dihasilkan oleh database, sementara token kedua menggunakan nilai yang diatur secara eksplisit:
Token {Id: 1} Unchanged
Id: 1 PK
Name: 'A'
ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
Id: 2 PK
Name: 'B'
ValidFrom: '11/11/1111 11:11:11 AM'
Catatan
Menggunakan nilai default database mengharuskan kolom database memiliki batasan nilai default yang dikonfigurasi. Ini dilakukan secara otomatis oleh migrasi EF Core saat menggunakan HasDefaultValueSql atau HasDefaultValue. Pastikan untuk membuat batasan default pada kolom dengan cara lain saat tidak menggunakan migrasi EF Core.
Menggunakan properti nullable
EF Core dapat menentukan apakah properti telah ditetapkan dengan membandingkan nilai properti dengan default CLR untuk jenis tersebut atau tidak. Ini berfungsi dengan baik dalam kebanyakan kasus, tetapi berarti bahwa default CLR tidak dapat dimasukkan secara eksplisit ke dalam database. Misalnya, pertimbangkan entitas dengan properti bilangan bulat:
public class Foo1
{
public int Id { get; set; }
public int Count { get; set; }
}
Di mana properti tersebut dikonfigurasi untuk memiliki default database -1:
modelBuilder
.Entity<Foo1>()
.Property(e => e.Count)
.HasDefaultValue(-1);
Niatnya adalah bahwa default -1 akan digunakan setiap kali nilai eksplisit tidak diatur. Namun, mengatur nilai ke 0 (default CLR untuk bilangan bulat) tidak dapat dibedakan ke EF Core dari tidak mengatur nilai apa pun, ini berarti bahwa tidak mungkin untuk menyisipkan 0 untuk properti ini. Contohnya:
using var context = new BlogsContext();
var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);
Perhatikan bahwa instans di mana Count
secara eksplisit diatur ke 0 masih mendapatkan nilai default dari database, yang bukan yang kami maksudkan. Cara mudah untuk menangani hal ini adalah dengan membuat Count
properti nullable:
public class Foo2
{
public int Id { get; set; }
public int? Count { get; set; }
}
Ini membuat clr default null, bukan 0, yang berarti 0 sekarang akan dimasukkan ketika secara eksplisit diatur:
using var context = new BlogsContext();
var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Menggunakan bidang backing nullable
Masalah dengan membuat properti dapat diubah ke null sehingga mungkin tidak dapat diubah ke null secara konseptual dalam model domain. Memaksa properti menjadi nullable oleh karena itu membahayakan model.
Properti dapat dibiarkan tidak dapat diubah ke null, dengan hanya bidang penolakan yang dapat diubah ke null. Contohnya:
public class Foo3
{
public int Id { get; set; }
private int? _count;
public int Count
{
get => _count ?? -1;
set => _count = value;
}
}
Ini memungkinkan default CLR (0) dimasukkan jika properti secara eksplisit diatur ke 0, sementara tidak perlu mengekspos properti sebagai nullable dalam model domain. Contohnya:
using var context = new BlogsContext();
var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();
context.AddRange(fooA, fooB, fooC);
context.SaveChanges();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Bidang penolakan nullable untuk properti bool
Pola ini sangat berguna saat menggunakan properti bool dengan default yang dihasilkan toko. Karena default CLR untuk bool
adalah "false", itu berarti bahwa "false" tidak dapat dimasukkan secara eksplisit menggunakan pola normal. Misalnya, pertimbangkan User
jenis entitas:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
private bool? _isAuthorized;
public bool IsAuthorized
{
get => _isAuthorized ?? true;
set => _isAuthorized = value;
}
}
Properti IsAuthorized
dikonfigurasi dengan nilai default database "true":
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
Properti IsAuthorized
dapat diatur ke "true" atau "false" secara eksplisit sebelum menyisipkan, atau dapat dibiarkan tidak diatur dalam hal ini default database akan digunakan:
using var context = new BlogsContext();
var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!
context.AddRange(userA, userB, userC);
context.SaveChanges();
Output dari SaveChanges saat menggunakan SQLite menunjukkan bahwa default database digunakan untuk Mac, sementara nilai eksplisit diatur untuk Alice dan Baxter:
-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Hanya default skema
Terkadang berguna untuk memiliki default dalam skema database yang dibuat oleh migrasi EF Core tanpa EF Core pernah menggunakan nilai-nilai ini untuk penyisipan. Ini dapat dicapai dengan mengonfigurasi properti sebagai PropertyBuilder.ValueGeneratedNever Misalnya:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();
Saran dan Komentar
https://aka.ms/ContentUserFeedback.
Segera hadir: Sepanjang tahun 2024 kami akan menghentikan penggunaan GitHub Issues sebagai mekanisme umpan balik untuk konten dan menggantinya dengan sistem umpan balik baru. Untuk mengetahui informasi selengkapnya, lihat:Kirim dan lihat umpan balik untuk