Bagikan melalui


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, , 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 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, , UpdateAttach, 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:

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();