Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Dokumen ini mencakup fitur dan skenario lain-lain yang melibatkan pelacakan perubahan.
Petunjuk / Saran
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.
Petunjuk / Saran
Anda dapat menjalankan dan men-debug ke semua kode dalam dokumen ini dengan mengunduh kode sampel dari GitHub.
Add melawan 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 disertakan dalam 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, , UpdateAttach, 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 pemanggilan ke metode non-rentang yang memiliki fungsi setara. Tidak ada perbedaan performa yang signifikan antara kedua pendekatan tersebut.
Nota
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 menjadi lebih efisien pada 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 DbSet<TEntity> dan DbContext. Metode ini memiliki perilaku yang sama persis untuk jenis entitas normal. Ini karena tipe CLR dari entitas dipetakan ke satu jenis entitas saja 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 tipe bersama, yang terutama digunakan untuk entitas penggabungan banyak-ke-banyak. Saat menggunakan tipe entitas berjenis berbagi, DbSet harus terlebih dahulu dibuat untuk tipe 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 berbagi digunakan secara default untuk entitas gabungan dalam hubungan banyak ke banyak. Jenis entitas yang bersifat 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 instance entitas gabungan yang 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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);
await context.SaveChangesAsync();
Perhatikan bahwa DbContext.Set<TEntity>(String) digunakan untuk membuat DbSet untuk PostTag jenis entitas. DbSet ini kemudian dapat digunakan untuk memanggil Add menggunakan instance entitas penggabung yang 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 secara default menggunakan bidang yang mendukung properti tersebut. Ini efisien dan menghindari memicu efek samping dari pemanggilan properti getter dan setter. Misalnya, inilah cara lazy loading dapat menghindari pemicu terjadinya 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 melakukan pengikatan data ke entitas, pengaturan suatu properti dapat menghasilkan pemberitahuan ke antarmuka pengguna, yang tidak terjadi saat mengatur kolom 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 akses properti Field dan PreferField akan menyebabkan EF Core mengakses nilai properti melalui kolom pendukungnya. 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 ketika Anda mengharapkannya.
Di sisi lain, mode PreferField dan PreferProperty akan beralih kembali menggunakan properti atau bidang pencadangan masing-masing jika akses yang disukai tidak dapat digunakan.
PreferField adalah default. Ini berarti EF Core akan menggunakan field sejauh dimungkinkan, tetapi tidak akan gagal jika properti harus diakses melalui getter atau setter-nya.
FieldDuringConstruction dan PreferFieldDuringConstruction konfigurasikan EF Core agar menggunakan bidang pendukung hanya saat membuat instans entitas. Ini memungkinkan kueri dijalankan tanpa efek samping getter dan setter, sementara perubahan properti oleh EF Core nantinya dapat menyebabkan efek samping ini.
Mode akses properti yang berbeda dirangkum dalam tabel berikut:
| PropertyAccessMode | Preferensi | Entitas pembuat preferensi | Alternatif | Fallback membuat entitas |
|---|---|---|---|---|
Field |
Bidang | Bidang | Melemparkan | Melemparkan |
Property |
Harta benda | Harta benda | Melemparkan | Melemparkan |
PreferField |
Bidang | Bidang | Harta benda | Harta benda |
PreferProperty |
Harta benda | Harta benda | Bidang | Bidang |
FieldDuringConstruction |
Harta benda | Bidang | Bidang | Melemparkan |
PreferFieldDuringConstruction |
Harta benda | Bidang | Bidang | Harta benda |
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 diterapkan 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);
await context.SaveChangesAsync();
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.BlogIdFK diberi nilai negatif yang sama dengan PK blog terkait. - Nilai PK ditandai sebagai sementara dengan menyetel IsTemporary setelah tiap 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) });
await context.SaveChangesAsync();
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'
Nota
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 oleh EF Core dari tidak mengatur nilai apa pun, ini berarti bahwa Anda tidak bisa 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);
await context.SaveChangesAsync();
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);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Menggunakan field pendukung nullable
Masalah dengan membuat properti dapat memiliki nilai null adalah bahwa mungkin properti tersebut tidak seharusnya memiliki nilai null secara konseptual dalam model domain. Memaksa properti menjadi nullable oleh karena itu membahayakan model.
Properti dapat dibiarkan tidak dapat bernilai null, dengan hanya field pendukung yang dapat bernilai 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 diatur secara eksplisit ke 0, sambil tidak perlu memperlihatkan properti tersebut 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);
await context.SaveChangesAsync();
Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);
Bidang pendukung nullable untuk properti bool
Pola ini sangat berguna saat menggunakan properti Boolean dengan nilai bawaan yang dihasilkan basis data. 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 disetel menjadi "true" atau "false" secara eksplisit sebelum menyisipkan, atau dapat dibiarkan tidak disetel sehingga 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);
await context.SaveChangesAsync();
Output dari SaveChanges saat menggunakan SQLite menunjukkan bahwa default database digunakan untuk Mac, sedangkan nilai eksplisit ditetapkan 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 pengaturan skema
Terkadang berguna memiliki nilai default dalam skema database yang dibuat oleh migrasi EF Core, meskipun EF Core tidak 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();