Yang Baru di EF Core 7.0

EF Core 7.0 (EF7) dirilis pada November 2022.

Tip

Anda dapat menjalankan dan men-debug ke dalam sampel dengan mengunduh kode sampel dari GitHub. Setiap bagian menautkan ke kode sumber khusus untuk bagian tersebut.

EF7 menargetkan .NET 6, sehingga dapat digunakan dengan .NET 6 (LTS) atau .NET 7.

Contoh model

Banyak contoh di bawah ini menggunakan model sederhana dengan blog, posting, tag, dan penulis:

public class Blog
{
    public Blog(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Post
{
    public Post(string title, string content, DateTime publishedOn)
    {
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
    }

    public int Id { get; private set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedOn { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
    public Author? Author { get; set; }
    public PostMetadata? Metadata { get; set; }
}

public class FeaturedPost : Post
{
    public FeaturedPost(string title, string content, DateTime publishedOn, string promoText)
        : base(title, content, publishedOn)
    {
        PromoText = promoText;
    }

    public string PromoText { get; set; }
}

public class Tag
{
    public Tag(string id, string text)
    {
        Id = id;
        Text = text;
    }

    public string Id { get; private set; }
    public string Text { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Author
{
    public Author(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; } = null!;
    public List<Post> Posts { get; } = new();
}

Beberapa contoh juga menggunakan jenis agregat, yang dipetakan dengan cara yang berbeda dalam sampel yang berbeda. Ada satu jenis agregat untuk kontak:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Dan jenis agregat kedua untuk metadata pos:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Tip

Model sampel dapat ditemukan di BlogsContext.cs.

Kolom JSON

Sebagian besar database relasional mendukung kolom yang berisi dokumen JSON. JSON dalam kolom ini dapat dibor dengan kueri. Ini memungkinkan, misalnya, pemfilteran dan pengurutan berdasarkan elemen dokumen, serta proyeksi elemen dari dokumen ke dalam hasil. Kolom JSON memungkinkan database relasional untuk mengambil beberapa karakteristik database dokumen, membuat hibrid yang berguna di antara keduanya.

EF7 berisi dukungan penyedia-agnostik untuk kolom JSON, dengan implementasi untuk SQL Server. Dukungan ini memungkinkan pemetaan agregat yang dibangun dari jenis .NET ke dokumen JSON. Kueri LINQ normal dapat digunakan pada agregat, dan ini akan diterjemahkan ke konstruksi kueri yang sesuai yang diperlukan untuk menelusuri JSON. EF7 juga mendukung pembaruan dan penyimpanan perubahan pada dokumen JSON.

Catatan

Dukungan SQLite untuk JSON direncanakan untuk pasca EF7. Penyedia PostgreSQL dan Pomelo MySQL sudah berisi beberapa dukungan untuk kolom JSON. Kami akan bekerja sama dengan penulis penyedia tersebut untuk menyelaraskan dukungan JSON di semua penyedia.

Pemetaan ke kolom JSON

Dalam EF Core, jenis agregat didefinisikan menggunakan OwnsOne dan OwnsMany. Misalnya, pertimbangkan jenis agregat dari model sampel kami yang digunakan untuk menyimpan informasi kontak:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Ini kemudian dapat digunakan dalam jenis entitas "pemilik", misalnya, untuk menyimpan detail kontak penulis:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; }
}

Jenis agregat dikonfigurasi dalam OnModelCreating menggunakan OwnsOne:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Tip

Kode yang ditampilkan di sini berasal dari JsonColumnsSample.cs.

Secara default, penyedia database relasional memetakan jenis agregat seperti ini ke tabel yang sama dengan jenis entitas pemilik. Artinya, setiap properti kelas dan Address dipetakan ContactDetails ke kolom dalam Authors tabel.

Beberapa penulis yang disimpan dengan detail kontak akan terlihat seperti ini:

Penulis

Id Nama Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Telepon
1 Maddy Montaquila 1 St Utama Hijau Camberwick CW1 5ZH Inggris 01632 12345
2 Jeremy Likness 2 St Utama Chigley CW1 5ZH Inggris 01632 12346
3 Daniel Roth 3 St Utama Hijau Camberwick CW1 5ZH Inggris 01632 12347
4 Arthur Vickers 15a Main St Chigley CW1 5ZH Inggris Raya 01632 22345
5 Brice Lambson 4 St Utama Chigley CW1 5ZH Inggris 01632 12349

Jika diinginkan, setiap jenis entitas yang membentuk agregat dapat dipetakan ke tabelnya sendiri sebagai gantinya:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToTable("Contacts");
            ownedNavigationBuilder.OwnsOne(
                contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder =>
                {
                    ownedOwnedNavigationBuilder.ToTable("Addresses");
                });
        });
}

Data yang sama kemudian disimpan di tiga tabel:

Penulis

Id Nama
1 Maddy Montaquila
2 Jeremy Likness
3 Daniel Roth
4 Arthur Vickers
5 Brice Lambson

Kontak

AuthorId Nomor
1 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

Alamat

ContactDetailsAuthorId Jalan Kota Kodepos Negara
1 1 St Utama Hijau Camberwick CW1 5ZH Inggris
2 2 St Utama Chigley CW1 5ZH Inggris
3 3 St Utama Hijau Camberwick CW1 5ZH Inggris
4 15a Main St Chigley CW1 5ZH Inggris Raya
5 4 St Utama Chigley CW1 5ZH Inggris

Sekarang, untuk bagian yang menarik. Di EF7, ContactDetails jenis agregat dapat dipetakan ke kolom JSON. Ini hanya memerlukan satu panggilan ke ToJson() saat mengonfigurasi jenis agregat:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToJson();
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Tabel Authors sekarang akan berisi kolom JSON untuk ContactDetails diisi dengan dokumen JSON untuk setiap penulis:

Penulis

Id Nama Kontak
1 Maddy Montaquila {
  "Telepon":"01632 12345",
  "Alamat": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"1 Main St"
  }
}
2 Jeremy Likness {
  "Telepon":"01632 12346",
  "Alamat": {
    "Kota":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"2 Main St"
  }
}
3 Daniel Roth {
  "Telepon":"01632 12347",
  "Alamat": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"3 Main St"
  }
}
4 Arthur Vickers {
  "Telepon":"01632 12348",
  "Alamat": {
    "Kota":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"15a Main St"
  }
}
5 Brice Lambson {
  "Telepon":"01632 12349",
  "Alamat": {
    "Kota":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"4 Main St"
  }
}

Tip

Penggunaan agregat ini sangat mirip dengan cara dokumen JSON dipetakan saat menggunakan penyedia EF Core untuk Azure Cosmos DB. Kolom JSON menghadirkan kemampuan menggunakan EF Core terhadap database dokumen ke dokumen yang disematkan dalam database relasional.

Dokumen JSON yang ditunjukkan di atas sangat sederhana, tetapi kemampuan pemetaan ini juga dapat digunakan dengan struktur dokumen yang lebih kompleks. Misalnya, pertimbangkan jenis agregat lain dari model sampel kami, yang digunakan untuk mewakili metadata tentang postingan:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Jenis agregat ini berisi beberapa jenis dan koleksi berlapis. Panggilan ke OwnsOne dan OwnsMany digunakan untuk memetakan jenis agregat ini:

modelBuilder.Entity<Post>().OwnsOne(
    post => post.Metadata, ownedNavigationBuilder =>
    {
        ownedNavigationBuilder.ToJson();
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches);
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies);
        ownedNavigationBuilder.OwnsMany(
            metadata => metadata.Updates,
            ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits));
    });

Tip

ToJson hanya diperlukan pada akar agregat untuk memetakan seluruh agregat ke dokumen JSON.

Dengan pemetaan ini, EF7 dapat membuat dan mengkueri ke dalam dokumen JSON yang kompleks seperti ini:

{
  "Views": 5085,
  "TopGeographies": [
    {
      "Browsers": "Firefox, Netscape",
      "Count": 924,
      "Latitude": 110.793,
      "Longitude": 39.2431
    },
    {
      "Browsers": "Firefox, Netscape",
      "Count": 885,
      "Latitude": 133.793,
      "Longitude": 45.2431
    }
  ],
  "TopSearches": [
    {
      "Count": 9359,
      "Term": "Search #1"
    }
  ],
  "Updates": [
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1996-02-17T19:24:29.5429092Z",
      "Commits": []
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "2019-11-24T19:24:29.5429093Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1997-05-28T19:24:29.5429097Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        },
        {
          "Comment": "Commit #2",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    }
  ]
}

Catatan

Pemetaan jenis spasial langsung ke JSON belum didukung. Dokumen di atas menggunakan double nilai sebagai solusinya. Pilih jenis spasial Dukungan di kolom JSON jika ini adalah sesuatu yang Anda minati.

Catatan

Pemetaan koleksi jenis primitif ke JSON belum didukung. Dokumen di atas menggunakan pengonversi nilai untuk mengubah koleksi menjadi string yang dipisahkan koma. Pilih Json : tambahkan dukungan untuk pengumpulan jenis primitif jika ini adalah sesuatu yang Anda minati.

Catatan

Pemetaan jenis yang dimiliki ke JSON belum didukung bersama dengan warisan TPT atau TPC. Pilih properti JSON Dukungan dengan pemetaan warisan TPT/TPC jika ini adalah sesuatu yang Anda minati.

Kueri ke dalam kolom JSON

Kueri ke dalam kolom JSON berfungsi sama seperti mengkueri ke jenis agregat lain di EF Core. Artinya, gunakan saja LINQ! Berikut adalah beberapa contoh.

Kueri untuk semua penulis yang tinggal di Chigley:

var authorsInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .ToListAsync();

Kueri ini menghasilkan SQL berikut saat menggunakan SQL Server:

SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

Perhatikan penggunaan JSON_VALUE untuk mendapatkan City dari dalam Address dokumen JSON.

Select dapat digunakan untuk mengekstrak dan memproyeksikan elemen dari dokumen JSON:

var postcodesInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .Select(author => author.Contact.Address.Postcode)
    .ToListAsync();

Kueri ini menghasilkan SQL berikut:

SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

Berikut adalah contoh yang melakukan sedikit lebih banyak dalam filter dan proyeksi, dan juga memesan berdasarkan nomor telepon dalam dokumen JSON:

var orderedAddresses = await context.Authors
    .Where(
        author => (author.Contact.Address.City == "Chigley"
                   && author.Contact.Phone != null)
                  || author.Name.StartsWith("D"))
    .OrderBy(author => author.Contact.Phone)
    .Select(
        author => author.Name + " (" + author.Contact.Address.Street
                  + ", " + author.Contact.Address.City
                  + " " + author.Contact.Address.Postcode + ")")
    .ToListAsync();

Kueri ini menghasilkan SQL berikut:

SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')'
FROM [Authors] AS [a]
WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%')
ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max))

Dan ketika dokumen JSON berisi koleksi, maka ini dapat diproyeksikan dalam hasil:

var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
    .AsNoTracking()
    .Select(
        post => new
        {
            post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
        })
    .ToListAsync();

Kueri ini menghasilkan SQL berikut:

SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000

Catatan

Kueri yang lebih kompleks yang melibatkan koleksi JSON memerlukan jsonpath dukungan. Pilih dukungan jsonpath kueri jika ini adalah sesuatu yang Anda minati.

Tip

Pertimbangkan untuk membuat indeks untuk meningkatkan performa kueri di dokumen JSON. Misalnya, lihat Mengindeks data Json saat menggunakan SQL Server.

Memperbarui kolom JSON

SaveChanges dan SaveChangesAsync bekerja dengan cara normal untuk membuat pembaruan pada kolom JSON. Untuk perubahan ekstensif, seluruh dokumen akan diperbarui. Misalnya, mengganti sebagian Contact besar dokumen untuk penulis:

var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));

jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };

await context.SaveChangesAsync();

Dalam hal ini, seluruh dokumen baru diteruskan sebagai parameter:

info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30']

Yang kemudian digunakan dalam UPDATE SQL:

UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;

Namun, jika hanya sub-dokumen yang diubah, maka EF Core akan menggunakan JSON_MODIFY perintah untuk memperbarui hanya sub-dokumen. Misalnya, mengubah bagian Address dalam Contact dokumen:

var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();

Menghasilkan parameter berikut:

info: 10/2/2022 15:51:15.895 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30']

Yang digunakan dalam UPDATE melalui JSON_MODIFY panggilan:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;

Terakhir, jika hanya satu properti yang diubah, maka EF Core akan kembali menggunakan perintah "JSON_MODIFY", kali ini untuk menambal hanya nilai properti yang diubah. Contohnya:

var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();

Menghasilkan parameter berikut:

info: 10/2/2022 15:54:05.112 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']

Yang lagi-lagi digunakan dengan JSON_MODIFY:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;

ExecuteUpdate dan ExecuteDelete (Pembaruan massal)

Secara default, EF Core melacak perubahan pada entitas, lalu mengirim pembaruan ke database saat salah satu metode dipanggil SaveChanges . Perubahan hanya dikirim untuk properti dan hubungan yang benar-benar berubah. Selain itu, entitas yang dilacak tetap sinkron dengan perubahan yang dikirim ke database. Mekanisme ini adalah cara yang efisien dan nyaman untuk mengirim sisipan, pembaruan, dan penghapusan tujuan umum ke database. Perubahan ini juga di-batch untuk mengurangi jumlah perjalanan pulang-pergi database.

Namun, terkadang berguna untuk menjalankan perintah pembaruan atau penghapusan pada database tanpa melibatkan pelacak perubahan. EF7 memungkinkan ini dengan metode dan ExecuteDelete baruExecuteUpdate. Metode ini diterapkan ke kueri LINQ dan akan memperbarui atau menghapus entitas dalam database berdasarkan hasil kueri tersebut. Banyak entitas dapat diperbarui dengan satu perintah dan entitas tidak dimuat ke dalam memori, yang berarti ini dapat menghasilkan pembaruan dan penghapusan yang lebih efisien.

Namun, perlu diingat bahwa:

  • Perubahan khusus yang harus dibuat harus ditentukan secara eksplisit; mereka tidak terdeteksi secara otomatis oleh EF Core.
  • Entitas terlacak apa pun tidak akan tetap sinkron.
  • Perintah tambahan mungkin perlu dikirim dalam urutan yang benar agar tidak melanggar batasan database. Misalnya, menghapus dependen sebelum prinsipal dapat dihapus.

Semua ini berarti bahwa ExecuteUpdate metode dan ExecuteDelete melengkapi, daripada menggantikan, mekanisme yang ada SaveChanges .

Contoh dasar ExecuteDelete

Tip

Kode yang ditampilkan di sini berasal dari ExecuteDeleteSample.cs.

Memanggil ExecuteDelete atau pada segera DbSet menghapus semua entitas tersebut DbSet dari ExecuteDeleteAsync database. Misalnya, untuk menghapus semua Tag entitas:

await context.Tags.ExecuteDeleteAsync();

Ini menjalankan SQL berikut saat menggunakan SQL Server:

DELETE FROM [t]
FROM [Tags] AS [t]

Lebih menarik lagi, kueri dapat berisi filter. Contohnya:

await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();

Ini menjalankan SQL berikut:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'

Kueri juga dapat menggunakan filter yang lebih kompleks, termasuk navigasi ke jenis lain. Misalnya, untuk menghapus tag hanya dari posting blog lama:

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();

Yang menjalankan:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

Contoh dasar ExecuteUpdate

Tip

Kode yang ditampilkan di sini berasal dari ExecuteUpdateSample.cs.

ExecuteUpdate dan ExecuteUpdateAsync berulah dengan cara ExecuteDelete yang sangat mirip dengan metode. Perbedaan utamanya adalah pembaruan memerlukan mengetahui properti mana yang akan diperbarui , dan cara memperbaruinya. Ini dicapai menggunakan satu atau beberapa panggilan ke SetProperty. Misalnya, untuk memperbarui Name setiap blog:

await context.Blogs.ExecuteUpdateAsync(
    s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));

Parameter SetProperty pertama menentukan properti mana yang akan diperbarui; dalam hal ini, Blog.Name. Parameter kedua menentukan bagaimana nilai baru harus dihitung; dalam hal ini, dengan mengambil nilai yang ada dan menambahkan "*Featured!*". SQL yang dihasilkan adalah:

UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]

ExecuteDeleteSeperti halnya , kueri dapat digunakan untuk memfilter entitas mana yang diperbarui. Selain itu, beberapa panggilan untuk SetProperty dapat digunakan untuk memperbarui lebih dari satu properti pada entitas target. Misalnya, untuk memperbarui Title dan Content dari semua postingan yang diterbitkan sebelum 2022:

await context.Posts
    .Where(p => p.PublishedOn.Year < 2022)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
        .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));

Dalam hal ini SQL yang dihasilkan sedikit lebih rumit:

UPDATE [p]
SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
    [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022

Terakhir, sekali lagi seperti ExecuteDelete, filter dapat mereferensikan tabel lain. Misalnya, untuk memperbarui semua tag dari posting lama:

await context.Tags
    .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

Yang menghasilkan:

UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

Untuk informasi selengkapnya dan sampel kode pada ExecuteUpdate dan , lihat ExecuteUpdate dan ExecuteDeleteExecuteDelete.

Warisan dan beberapa tabel

ExecuteUpdate dan ExecuteDelete hanya dapat bertindak pada satu tabel. Ini memiliki implikasi ketika bekerja dengan strategi pemetaan warisan yang berbeda. Umumnya, tidak ada masalah saat menggunakan strategi pemetaan TPH, karena hanya ada satu tabel untuk dimodifikasi. Misalnya, menghapus semua FeaturedPost entitas:

await context.Set<FeaturedPost>().ExecuteDeleteAsync();

Menghasilkan SQL berikut saat menggunakan pemetaan TPH:

DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'

Tidak ada juga masalah untuk kasus ini saat menggunakan strategi pemetaan TPC, karena sekali lagi hanya perubahan pada satu tabel yang diperlukan:

DELETE FROM [f]
FROM [FeaturedPosts] AS [f]

Namun, mencoba ini saat menggunakan strategi pemetaan TPT akan gagal karena akan memerlukan penghapusan baris dari dua tabel yang berbeda.

Menambahkan filter ke kueri sering berarti operasi akan gagal dengan strategi TPC dan TPT. Ini lagi karena baris mungkin perlu dihapus dari beberapa tabel. Misalnya, kueri ini:

await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();

Menghasilkan SQL berikut saat menggunakan TPH:

DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')

Tetapi gagal saat menggunakan TPC atau TPT.

Tip

Masalah #10879 melacak menambahkan dukungan untuk mengirim beberapa perintah secara otomatis dalam skenario ini. Pilih masalah ini jika itu adalah sesuatu yang ingin Anda lihat diimplementasikan.

ExecuteDelete dan hubungan

Seperti disebutkan di atas, mungkin perlu untuk menghapus atau memperbarui entitas dependen sebelum prinsip hubungan dapat dihapus. Misalnya, masing-masing Post adalah dependen dari yang terkait Author. Ini berarti bahwa penulis tidak dapat dihapus jika postingan masih mereferensikannya; melakukannya akan melanggar batasan kunci asing dalam database. Misalnya, mencoba ini:

await context.Authors.ExecuteDeleteAsync();

Akan menghasilkan pengecualian berikut di SQL Server:

Microsoft.Data.SqlClient.SqlException (0x80131904): Pernyataan DELETE bertentangan dengan batasan REFERENSI "FK_Posts_Authors_AuthorId". Konflik terjadi dalam database "TphBlogsContext", tabel "dbo. Postingan", kolom 'AuthorId'. Pernyataan ini telah dihapus.

Untuk memperbaiki ini, kita harus terlebih dahulu menghapus postingan, atau memutuskan hubungan antara setiap posting dan penulisnya dengan mengatur AuthorId properti kunci asing ke null. Misalnya, menggunakan opsi hapus:

await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();

Tip

TagWith dapat digunakan untuk menandai ExecuteDelete atau ExecuteUpdate dengan cara yang sama karena menandai kueri normal.

Ini menghasilkan dua perintah terpisah; yang pertama untuk menghapus dependen:

-- Deleting posts...

DELETE FROM [p]
FROM [Posts] AS [p]

Dan yang kedua untuk menghapus prinsipal:

-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]

Penting

Beberapa ExecuteDelete perintah dan ExecuteUpdate tidak akan terkandung dalam satu transaksi secara default. Namun, API transaksi DbContext dapat digunakan dengan cara normal untuk membungkus perintah ini dalam transaksi.

Tip

Mengirim perintah ini dalam satu perjalanan pulang pergi tergantung pada Masalah #10879. Pilih masalah ini jika itu adalah sesuatu yang ingin Anda lihat diimplementasikan.

Mengonfigurasi penghapusan kaskade dalam database bisa sangat berguna di sini. Dalam model kami, hubungan antara Blog dan Post diperlukan, yang menyebabkan EF Core mengonfigurasi penghapusan kaskade menurut konvensi. Ini berarti ketika blog dihapus dari database, maka semua posting dependennya juga akan dihapus. Kemudian mengikuti bahwa untuk menghapus semua blog dan postingan, kita hanya perlu menghapus blog:

await context.Blogs.ExecuteDeleteAsync();

Ini menghasilkan SQL berikut:

DELETE FROM [b]
FROM [Blogs] AS [b]

Yang, karena menghapus blog, juga akan menyebabkan semua posting terkait dihapus oleh penghapusan bertingkat yang dikonfigurasi.

SaveChanges yang Lebih Cepat

Di EF7, kinerja SaveChanges dan SaveChangesAsync telah meningkat secara signifikan. Dalam beberapa skenario, menyimpan perubahan sekarang hingga empat kali lebih cepat daripada dengan EF Core 6.0!

Sebagian besar peningkatan ini berasal dari:

  • Melakukan lebih sedikit perjalanan pulang pergi ke database
  • Menghasilkan SQL yang lebih cepat

Beberapa contoh peningkatan ini ditunjukkan di bawah ini.

Catatan

Lihat Mengumumkan Entity Framework Core 7 Preview 6: Edisi Performa di Blog .NET untuk diskusi mendalam tentang perubahan ini.

Tip

Kode yang ditampilkan di sini berasal dari SaveChangesPerformanceSample.cs.

Transaksi yang tidak diperlukan dihilangkan

Semua database relasional modern menjamin transaksionalitas untuk (sebagian besar) pernyataan SQL tunggal. Artinya, pernyataan tidak akan pernah hanya diselesaikan sebagian, bahkan jika terjadi kesalahan. EF7 menghindari memulai transaksi eksplisit dalam kasus ini.

Misalnya, melihat pengelogan untuk panggilan berikut ke SaveChanges:

await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();

Menunjukkan bahwa dalam EF Core 6.0, INSERT perintah dibungkus oleh perintah untuk memulai dan kemudian melakukan transaksi:

dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

EF7 mendeteksi bahwa transaksi tidak diperlukan di sini sehingga menghapus panggilan ini:

info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

Ini menghapus dua perjalanan pulang-pergi database, yang dapat membuat perbedaan besar terhadap performa keseluruhan, terutama ketika latensi panggilan ke database tinggi. Dalam sistem produksi umum, database tidak terletak di komputer yang sama dengan aplikasi. Ini berarti latensi sering relatif tinggi, membuat pengoptimalan ini sangat efektif dalam sistem produksi dunia nyata.

SQL yang disempurnakan untuk penyisipan Identitas sederhana

Kasus di atas menyisipkan satu baris dengan IDENTITY kolom kunci dan tidak ada nilai lain yang dihasilkan database. EF7 menyederhanakan SQL dalam hal ini dengan menggunakan OUTPUT INSERTED. Meskipun penyederhanaan ini tidak berlaku untuk banyak kasus lain, masih penting untuk ditingkatkan karena penyisipan baris tunggal semacam ini sangat umum di banyak aplikasi.

Menyisipkan beberapa baris

Di EF Core 6.0, pendekatan default untuk menyisipkan beberapa baris didorong oleh batasan dalam dukungan SQL Server untuk tabel dengan pemicu. Kami ingin memastikan bahwa pengalaman default berfungsi bahkan untuk minoritas pengguna dengan pemicu dalam tabel mereka. Ini berarti bahwa kita tidak dapat menggunakan klausa sederhana OUTPUT , karena, di SQL Server, ini tidak berfungsi dengan pemicu. Sebagai gantinya, saat menyisipkan beberapa entitas, EF Core 6.0 menghasilkan beberapa SQL yang cukup berbelit-bel. Misalnya, panggilan ini ke SaveChanges:

for (var i = 0; i < 4; i++)
{
    await context.AddAsync(new Blog { Name = "Foo" + i });
}

await context.SaveChangesAsync();

Menghasilkan tindakan berikut saat dijalankan terhadap SQL Server dengan EF Core 6.0:

dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [i].[Id] FROM @inserted0 i
      ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Penting

Meskipun ini rumit, batch beberapa sisipan seperti ini masih jauh lebih cepat daripada mengirim satu perintah untuk setiap sisipan.

Di EF7, Anda masih bisa mendapatkan SQL ini jika tabel Anda berisi pemicu, tetapi untuk kasus umum kita sekarang menghasilkan jauh lebih efisien, jika masih agak kompleks, perintah:

info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position;

Transaksi hilang, seperti dalam kasus penyisipan tunggal, karena MERGE merupakan pernyataan tunggal yang dilindungi oleh transaksi implisit. Selain itu, tabel sementara hilang dan klausul OUTPUT sekarang mengirim ID yang dihasilkan langsung kembali ke klien. Ini bisa empat kali lebih cepat daripada pada EF Core 6.0, tergantung pada faktor lingkungan seperti latensi antara aplikasi dan database.

Pemicu

Jika tabel memiliki pemicu, panggilan ke SaveChanges dalam kode di atas akan melemparkan pengecualian:

Pengecualian tidak dapat ditangani. Microsoft.EntityFrameworkCore.DbUpdateException:
Tidak dapat menyimpan perubahan karena tabel target memiliki pemicu database. Harap konfigurasikan jenis entitas Anda sesuai, lihat https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers untuk informasi selengkapnya.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
Tabel target 'BlogsWithTriggers' dari pernyataan DML tidak dapat memiliki pemicu yang diaktifkan jika pernyataan berisi klausa OUTPUT tanpa klausa INTO.

Kode berikut dapat digunakan untuk memberi tahu EF Core bahwa tabel memiliki pemicu:

modelBuilder
    .Entity<BlogWithTrigger>()
    .ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));

EF7 kemudian akan kembali ke EF Core 6.0 SQL saat mengirim perintah sisipkan dan perbarui untuk tabel ini.

Untuk informasi selengkapnya, termasuk konvensi untuk mengonfigurasi semua tabel yang dipetakan secara otomatis dengan pemicu, lihat tabel SQL Server dengan pemicu sekarang memerlukan konfigurasi EF Core khusus dalam dokumentasi perubahan pemecahan EF7.

Lebih sedikit perjalanan pulang-pergi untuk menyisipkan grafik

Pertimbangkan untuk menyisipkan grafik entitas yang berisi entitas utama baru dan juga entitas dependen baru dengan kunci asing yang mereferensikan prinsipal baru. Contohnya:

await context.AddAsync(
    new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();

Jika kunci utama utama dihasilkan oleh database, maka nilai yang akan diatur untuk kunci asing dalam dependen tidak diketahui sampai prinsipal telah dimasukkan. EF Core menghasilkan dua perjalanan pulang-pergi untuk ini--satu untuk memasukkan prinsipal dan mendapatkan kembali kunci primer baru, dan satu detik untuk memasukkan dependen dengan nilai kunci asing yang ditetapkan. Dan karena ada dua pernyataan untuk ini, transaksi diperlukan, yang berarti total ada empat perjalanan pulang-pergi:

dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);
info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Post] USING (
      VALUES (@p1, @p2, 0),
      (@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([BlogId], [Title])
      VALUES (i.[BlogId], i.[Title])
      OUTPUT INSERTED.[Id], i._Position;
dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Namun, dalam beberapa kasus, nilai kunci utama diketahui sebelum prinsipal disisipkan. Drive ini termasuk:

  • Nilai kunci yang tidak dibuat secara otomatis
  • Nilai kunci yang dihasilkan pada klien, seperti Guid kunci
  • Nilai kunci yang dihasilkan di server dalam batch, seperti saat menggunakan generator nilai hi-lo

Dalam EF7, kasus-kasus ini sekarang dioptimalkan menjadi satu perjalanan pulang pergi. Misalnya, dalam kasus di atas di SQL Server, Blog.Id kunci utama dapat dikonfigurasi untuk menggunakan strategi pembuatan hi-lo:

modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();

SaveChanges Panggilan dari atas sekarang dioptimalkan ke satu perjalanan pulang pergi untuk sisipan.

dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Posts] ([Id], [BlogId], [Title])
      VALUES (@p2, @p3, @p4),
      (@p5, @p6, @p7);
dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Perhatikan bahwa transaksi masih diperlukan di sini. Ini karena penyisipan dibuat menjadi dua tabel terpisah.

EF7 juga menggunakan satu batch dalam kasus lain di mana EF Core 6.0 akan membuat lebih dari satu batch. Misalnya, saat menghapus dan menyisipkan baris ke dalam tabel yang sama.

Nilai SaveChanges

Seperti yang ditunjukkan beberapa contoh di sini, menyimpan hasil ke database bisa menjadi bisnis yang kompleks. Di sinilah menggunakan sesuatu seperti EF Core benar-benar menunjukkan nilainya. EF Core:

  • Batch beberapa perintah sisipkan, perbarui, dan hapus bersama-sama untuk mengurangi roundtrips
  • Mencari tahu apakah transaksi eksplisit diperlukan atau tidak
  • Menentukan urutan untuk menyisipkan, memperbarui, dan menghapus entitas sehingga batasan database tidak dilanggar
  • Memastikan nilai yang dihasilkan database dikembalikan secara efisien dan disebarkan kembali ke entitas
  • Secara otomatis mengatur nilai kunci asing menggunakan nilai yang dihasilkan untuk kunci primer
  • Mendeteksi konflik konkurensi

Selain itu, sistem database yang berbeda memerlukan SQL yang berbeda untuk banyak kasus ini. Penyedia database EF Core bekerja dengan EF Core untuk memastikan perintah yang benar dan efisien dikirim untuk setiap kasus.

Pemetaan pewarisan tabel per jenis beton (TPC)

Secara default, EF Core memetakan hierarki pewarisan jenis .NET ke tabel database tunggal. Ini dikenal sebagai strategi pemetaan tabel per hierarki (TPH ). EF Core 5.0 memperkenalkan strategi table-per-type (TPT), yang mendukung pemetaan setiap jenis .NET ke tabel database yang berbeda. EF7 memperkenalkan strategi table-per-concrete-type (TPC). TPC juga memetakan jenis .NET ke tabel yang berbeda, tetapi dengan cara yang mengatasi beberapa masalah performa umum dengan strategi TPT.

Tip

Kode yang ditampilkan di sini berasal dari TpcInheritanceSample.cs.

Tip

Tim EF menunjukkan dan berbicara secara mendalam tentang pemetaan TPC dalam episode Standup Komunitas Data .NET. Seperti semua episode Community Standup, Anda dapat menonton episode TPC sekarang di YouTube.

Skema database TPC

Strategi TPC mirip dengan strategi TPT kecuali bahwa tabel yang berbeda dibuat untuk setiap jenis beton dalam hierarki, tetapi tabel tidak dibuat untuk jenis abstrak --karenanya nama "table-per-concrete-type". Seperti halnya TPT, tabel itu sendiri menunjukkan jenis objek yang disimpan. Namun, tidak seperti pemetaan TPT, setiap tabel berisi kolom untuk setiap properti dalam jenis beton dan jenis dasarnya. Skema database TPC dinormalisasi.

Misalnya, pertimbangkan untuk memetakan hierarki ini:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

Saat menggunakan SQL Server, tabel yang dibuat untuk hierarki ini adalah:

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

Perhatikan bahwa:

  • Tidak ada tabel untuk Animal jenis atau Pet , karena ini ada abstract dalam model objek. Ingatlah bahwa C# tidak mengizinkan instans jenis abstrak, dan oleh karena itu tidak ada situasi di mana instans jenis abstrak akan disimpan ke database.

  • Pemetaan properti dalam jenis dasar diulang untuk setiap jenis beton. Misalnya, setiap tabel memiliki Name kolom, dan Kucing dan Anjing memiliki Vet kolom.

  • Menyimpan beberapa data ke dalam database ini menghasilkan hal berikut:

Tabel Kucing

Id Nama FoodId Dokter hewan EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Prasekolah
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Rumah Sakit Hewan Peliharaan Bothell Bsc

Tabel anjing

Id Nama FoodId Dokter hewan FavoritToy
3 Toast 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Tn. Tupai

Tabel FarmAnimals

Id Nama FoodId Nilai Species
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100,00 Equus africanus asinus

Tabel manusia

Id Nama FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie nihil 8

Perhatikan bahwa, tidak seperti pemetaan TPT, semua informasi untuk satu objek terkandung dalam satu tabel. Dan, tidak seperti pemetaan TPH, tidak ada kombinasi kolom dan baris dalam tabel apa pun di mana itu tidak pernah digunakan oleh model. Kita akan melihat di bawah ini bagaimana karakteristik ini bisa penting untuk kueri dan penyimpanan.

Mengonfigurasi pewarisan TPC

Semua jenis dalam hierarki warisan harus secara eksplisit disertakan dalam model saat memetakan hierarki dengan EF Core. Ini dapat dilakukan dengan membuat DbSet properti pada Anda DbContext untuk setiap jenis:

public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Pet> Pets => Set<Pet>();
public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
public DbSet<Cat> Cats => Set<Cat>();
public DbSet<Dog> Dogs => Set<Dog>();
public DbSet<Human> Humans => Set<Human>();

Atau dengan menggunakan Entity metode di OnModelCreating:

modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();

Penting

Ini berbeda dari perilaku EF6 warisan, di mana jenis turunan dari jenis dasar yang dipetakan akan secara otomatis ditemukan jika terkandung dalam rakitan yang sama.

Tidak ada lagi yang perlu dilakukan untuk memetakan hierarki sebagai TPH, karena itu adalah strategi default. Namun, dimulai dengan EF7, TPH dapat dibuat eksplisit dengan memanggil UseTphMappingStrategy pada jenis dasar hierarki:

modelBuilder.Entity<Animal>().UseTphMappingStrategy();

Untuk menggunakan TPT, ubah ini menjadi UseTptMappingStrategy:

modelBuilder.Entity<Animal>().UseTptMappingStrategy();

Demikian juga, UseTpcMappingStrategy digunakan untuk mengonfigurasi TPC:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

Dalam setiap kasus, nama tabel yang digunakan untuk setiap jenis diambil dari DbSet nama properti pada Anda DbContext, atau dapat dikonfigurasi menggunakan ToTable metode penyusun, atau [Table] atribut .

Performa kueri TPC

Untuk kueri, strategi TPC adalah peningkatan atas TPT karena memastikan bahwa informasi untuk instans entitas tertentu selalu disimpan dalam satu tabel. Ini berarti strategi TPC dapat berguna ketika hierarki yang dipetakan besar dan memiliki banyak jenis beton (biasanya daun), masing-masing dengan sejumlah besar properti, dan di mana hanya sebagian kecil jenis yang digunakan dalam sebagian besar kueri.

SQL yang dihasilkan untuk tiga kueri LINQ sederhana dapat digunakan untuk mengamati di mana TPC melakukannya dengan baik jika dibandingkan dengan TPH dan TPT. Kueri ini adalah:

  1. Kueri yang mengembalikan entitas dari semua jenis dalam hierarki:

    context.Animals.ToList();
    
  2. Kueri yang mengembalikan entitas dari subset jenis dalam hierarki:

    context.Pets.ToList();
    
  3. Kueri yang hanya mengembalikan entitas dari satu jenis daun dalam hierarki:

    context.Cats.ToList();
    

Kueri TPH

Saat menggunakan TPH, ketiga kueri hanya mengkueri satu tabel, tetapi dengan filter yang berbeda pada kolom diskriminator:

  1. Entitas yang dikembalikan TPH SQL dari semua jenis dalam hierarki:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    
  2. TPH SQL mengembalikan entitas dari subset jenis dalam hierarki:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] IN (N'Cat', N'Dog')
    
  3. TPH SQL hanya mengembalikan entitas dari satu jenis daun dalam hierarki:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] = N'Cat'
    

Semua kueri ini harus berkinerja baik, terutama dengan indeks database yang sesuai pada kolom diskriminator.

Kueri TPT

Saat menggunakan TPT, semua kueri ini memerlukan gabungan beberapa tabel, karena data untuk jenis beton tertentu dibagi di banyak tabel:

  1. TPT SQL mengembalikan entitas dari semua jenis dalam hierarki:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
        WHEN [h].[Id] IS NOT NULL THEN N'Human'
        WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
    LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  2. TPT SQL mengembalikan entitas dari subset jenis dalam hierarki:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  3. TPT SQL hanya mengembalikan entitas dari satu jenis daun dalam hierarki:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    

Catatan

EF Core menggunakan "sintesis diskriminator" untuk menentukan tabel mana data berasal, dan karenanya jenis yang benar untuk digunakan. Ini berfungsi karena LEFT JOIN mengembalikan null untuk kolom ID dependen ("sub-tabel") yang bukan jenis yang benar. Jadi untuk anjing, [d].[Id] akan menjadi non-null, dan semua ID (beton) lainnya akan null.

Semua kueri ini dapat menderita masalah performa karena gabungan tabel. Inilah sebabnya mengapa TPT tidak pernah menjadi pilihan yang baik untuk performa kueri.

Kueri TPC

TPC meningkatkan TPT untuk semua kueri ini karena jumlah tabel yang perlu dikueri berkurang. Selain itu, hasil dari setiap tabel digabungkan menggunakan UNION ALL, yang bisa jauh lebih cepat daripada gabungan tabel, karena tidak perlu melakukan pencocokan antara baris atau de-duplikasi baris.

  1. TPC SQL mengembalikan entitas dari semua jenis dalam hierarki:

    SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
    FROM [Humans] AS [h]
    UNION ALL
    SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  2. TPC SQL mengembalikan entitas dari subset jenis dalam hierarki:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  3. TPC SQL hanya mengembalikan entitas dari satu jenis daun dalam hierarki:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
    FROM [Cats] AS [c]
    

Meskipun TPC lebih baik daripada TPT untuk semua kueri ini, kueri TPH masih lebih baik saat mengembalikan instans dari beberapa jenis. Ini adalah salah satu alasan bahwa TPH adalah strategi default yang digunakan oleh EF Core.

Seperti yang ditunjukkan SQL untuk kueri #3, TPC benar-benar unggul saat mengkueri entitas dari satu jenis daun. Kueri hanya menggunakan satu tabel dan tidak memerlukan pemfilteran.

Penyisipan dan pembaruan TPC

TPC juga berkinerja baik saat menyimpan entitas baru, karena ini hanya perlu menyisipkan satu baris ke dalam satu tabel. Ini juga berlaku untuk TPH. Dengan TPT, baris harus dimasukkan ke dalam banyak tabel, yang akan berkinerja kurang baik.

Hal yang sama sering berlaku untuk pembaruan, meskipun dalam hal ini jika semua kolom yang diperbarui berada dalam tabel yang sama, bahkan untuk TPT, perbedaannya mungkin tidak signifikan.

Pertimbangan ruang

Baik TPT maupun TPC dapat menggunakan lebih sedikit penyimpanan daripada TPH ketika ada banyak subjenis dengan banyak properti yang sering tidak digunakan. Ini karena setiap baris dalam tabel TPH harus menyimpan NULL untuk masing-masing properti yang tidak digunakan ini. Dalam praktiknya, ini jarang menjadi masalah, tetapi mungkin perlu dipertimbangkan ketika menyimpan data dalam jumlah besar dengan karakteristik ini.

Tip

Jika sistem database Anda mendukungnya (misalnya SQL Server), pertimbangkan untuk menggunakan "kolom jarang" untuk kolom TPH yang jarang diisi.

Pembuatan kunci

Strategi pemetaan warisan yang dipilih memiliki konsekuensi tentang bagaimana nilai kunci primer dihasilkan dan dikelola. Kunci dalam TPH mudah karena setiap instans entitas diwakili oleh satu baris dalam satu tabel. Segala jenis pembuatan nilai kunci dapat digunakan, dan tidak ada batasan tambahan yang diperlukan.

Untuk strategi TPT, selalu ada baris dalam tabel yang dipetakan ke jenis dasar hierarki. Segala jenis pembuatan kunci dapat digunakan pada baris ini, dan kunci untuk tabel lain ditautkan ke tabel ini menggunakan batasan kunci asing.

Hal-hal menjadi sedikit lebih rumit untuk TPC. Pertama, penting untuk dipahami bahwa EF Core mengharuskan semua entitas dalam hierarki harus memiliki nilai kunci yang unik, bahkan jika entitas memiliki jenis yang berbeda. Jadi, menggunakan model contoh kami, Anjing tidak dapat memiliki nilai kunci Id yang sama dengan Kucing. Kedua, tidak seperti TPT, tidak ada tabel umum yang dapat bertindak sebagai satu tempat di mana nilai kunci hidup dan dapat dihasilkan. Ini berarti kolom sederhana Identity tidak dapat digunakan.

Untuk database yang mendukung urutan, nilai kunci dapat dihasilkan dengan menggunakan urutan tunggal yang direferensikan dalam batasan default untuk setiap tabel. Ini adalah strategi yang digunakan dalam tabel TPC yang ditunjukkan di atas, di mana setiap tabel memiliki hal berikut:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence adalah urutan database yang dibuat oleh EF Core. Strategi ini digunakan secara default untuk hierarki TPC saat menggunakan penyedia database EF Core untuk SQL Server. Penyedia database untuk database lain yang mendukung urutan harus memiliki default yang sama. Strategi pembuatan kunci lainnya yang menggunakan urutan, seperti pola Hi-Lo, juga dapat digunakan dengan TPC.

Meskipun kolom Identitas standar tidak akan berfungsi dengan TPC, dimungkinkan untuk menggunakan kolom Identitas jika setiap tabel dikonfigurasi dengan benih dan kenaikan yang sesuai sehingga nilai yang dihasilkan untuk setiap tabel tidak akan pernah bertentangan. Contohnya:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

SQLite tidak mendukung urutan atau Seed/inkrement identitas, dan karenanya pembuatan nilai kunci bilangan bulat tidak didukung saat menggunakan SQLite dengan strategi TPC. Namun, pembuatan sisi klien atau kunci unik global--misalnya, kunci GUID--didukung pada database apa pun, termasuk SQLite.

Batasan kunci asing

Strategi pemetaan TPC membuat skema SQL denormalisasi--ini adalah salah satu alasan mengapa beberapa purist database menentangnya. Misalnya, pertimbangkan kolom FavoriteAnimalIdkunci asing . Nilai dalam kolom ini harus cocok dengan nilai kunci utama beberapa hewan. Ini dapat diberlakukan dalam database dengan batasan FK sederhana saat menggunakan TPH atau TPT. Contohnya:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Tetapi ketika menggunakan TPC, kunci utama untuk hewan disimpan di tabel untuk jenis beton hewan itu. Misalnya, kunci utama kucing disimpan di Cats.Id kolom, sementara kunci utama anjing disimpan di Dogs.Id kolom, dan sebagainya. Ini berarti batasan FK tidak dapat dibuat untuk hubungan ini.

Dalam praktiknya, ini bukan masalah selama aplikasi tidak mencoba menyisipkan data yang tidak valid. Misalnya, jika semua data dimasukkan oleh EF Core dan menggunakan navigasi untuk menghubungkan entitas, maka dijamin bahwa kolom FK akan berisi nilai PK yang valid setiap saat.

Ringkasan dan panduan

Singkatnya, TPC adalah strategi pemetaan yang baik untuk digunakan ketika kode Anda sebagian besar akan meminta entitas dari satu jenis daun. Ini karena persyaratan penyimpanan lebih kecil, dan tidak ada kolom diskriminator yang mungkin memerlukan indeks. Penyisipan dan pembaruan juga efisien.

Meskipun demikian, TPH biasanya baik-baik saja untuk sebagian besar aplikasi, dan merupakan default yang baik untuk berbagai skenario, jadi jangan tambahkan kompleksitas TPC jika Anda tidak membutuhkannya. Secara khusus, jika kode Anda sebagian besar akan mengkueri entitas dari banyak jenis, seperti menulis kueri terhadap jenis dasar, maka condong ke TPH melalui TPC.

Gunakan TPT hanya jika dibatasi untuk melakukannya oleh faktor eksternal.

Templat Rekayasa Terbalik Kustom

Anda sekarang dapat menyesuaikan kode perancah saat merekayasa balik model EF dari database. Mulailah dengan menambahkan templat default ke proyek Anda:

dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

Templat kemudian dapat disesuaikan dan akan secara otomatis digunakan oleh dotnet ef dbcontext scaffold dan Scaffold-DbContext.

Untuk detail selengkapnya, lihat Templat Rekayasa Terbalik Kustom.

Tip

Tim EF menunjukkan dan berbicara secara mendalam tentang templat rekayasa terbalik dalam episode Standup Komunitas Data .NET. Seperti semua episode Community Standup, Anda dapat menonton episode templat T4 sekarang di YouTube.

Konvensi pembuatan model

EF Core menggunakan "model" metadata untuk menjelaskan bagaimana jenis entitas aplikasi dipetakan ke database yang mendasar. Model ini dibangun menggunakan satu set sekitar 60 "konvensi". Model yang dibangun oleh konvensi kemudian dapat disesuaikan menggunakan atribut pemetaan (alias "anotasi data") dan/atau panggilan ke DbModelBuilder API di OnModelCreating.

Dimulai dengan EF7, aplikasi sekarang dapat menghapus atau mengganti salah satu konvensi ini, serta menambahkan konvensi baru. Konvensi pembuatan model adalah cara yang ampuh untuk mengontrol konfigurasi model, tetapi bisa kompleks dan sulit untuk disempurnakan. Dalam banyak kasus, konfigurasi model pra-konvensi yang ada dapat digunakan sebagai gantinya untuk dengan mudah menentukan konfigurasi umum untuk properti dan jenis.

Perubahan pada konvensi yang digunakan oleh dibuat DbContext dengan mengambil alih DbContext.ConfigureConventions metode . Contohnya:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Tip

Untuk menemukan semua konvensi pembuatan model bawaan, cari setiap kelas yang mengimplementasikan IConvention antarmuka.

Tip

Kode yang ditampilkan di sini berasal dari ModelBuildingConventionsSample.cs.

Menghapus konvensi yang sudah ada

Terkadang salah satu konvensi bawaan mungkin tidak sesuai untuk aplikasi Anda, dalam hal ini dapat dihapus.

Contoh: Jangan membuat indeks untuk kolom kunci asing

Biasanya masuk akal untuk membuat indeks untuk kolom kunci asing (FK), dan karenanya ada konvensi bawaan untuk ini: ForeignKeyIndexConvention. Melihat tampilan debug model untuk Post jenis entitas dengan hubungan ke Blog dan Author, kita dapat melihat dua indeks dibuat--satu untuk BlogId FK, dan yang lainnya untuk AuthorId FK.

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK Index
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      AuthorId
      BlogId

Namun, indeks memiliki overhead, dan, seperti yang ditanyakan di sini, mungkin tidak selalu sesuai untuk membuatnya untuk semua kolom FK. Untuk mencapai hal ini, ForeignKeyIndexConvention dapat dihapus saat membangun model:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Melihat tampilan debug model untuk Post saat ini, kita melihat bahwa indeks pada FK belum dibuat:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade

Jika diinginkan, indeks masih dapat dibuat secara eksplisit untuk kolom kunci asing, baik menggunakan IndexAttribute:

[Index("BlogId")]
public class Post
{
    // ...
}

Atau dengan konfigurasi di OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}

Melihat Post jenis entitas lagi, sekarang berisi BlogId indeks, tetapi bukan AuthorId indeks:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      BlogId

Tip

Jika model Anda tidak menggunakan atribut pemetaan (alias anotasi data) untuk konfigurasi, maka semua konvensi yang berakhirannya AttributeConvention dapat dihapus dengan aman untuk mempercepat pembuatan model.

Menambahkan konvensi baru

Menghapus konvensi yang ada adalah awal, tetapi bagaimana dengan menambahkan konvensi pembangunan model yang benar-benar baru? EF7 juga mendukung ini!

Contoh: Batasi panjang properti diskriminator

Strategi pemetaan warisan tabel per hierarki memerlukan kolom diskriminator untuk menentukan jenis mana yang diwakili dalam baris tertentu. Secara default, EF menggunakan kolom string yang tidak terbatas untuk diskriminator, yang memastikan bahwa itu akan berfungsi untuk panjang diskriminator apa pun. Namun, membatasi panjang maksimum string diskriminator dapat membuat penyimpanan dan kueri yang lebih efisien. Mari kita buat konvensi baru yang akan melakukan itu.

Konvensi pembuatan model EF Core dipicu berdasarkan perubahan yang dilakukan pada model saat sedang dibangun. Ini membuat model tetap terbaru saat konfigurasi eksplisit dibuat, atribut pemetaan diterapkan, dan konvensi lainnya berjalan. Untuk berpartisipasi dalam hal ini, setiap konvensi mengimplementasikan satu atau beberapa antarmuka yang menentukan kapan konvensi akan dipicu. Misalnya, konvensi yang menerapkan IEntityTypeAddedConvention akan dipicu setiap kali jenis entitas baru ditambahkan ke model. Demikian juga, konvensi yang mengimplementasikan keduanya IForeignKeyAddedConvention dan IKeyAddedConvention akan dipicu setiap kali kunci atau kunci asing ditambahkan ke model.

Mengetahui antarmuka mana yang akan diterapkan bisa sulit, karena konfigurasi yang dibuat untuk model pada satu titik dapat diubah atau dihapus di titik selanjutnya. Misalnya, kunci dapat dibuat oleh konvensi, tetapi kemudian diganti ketika kunci yang berbeda dikonfigurasi secara eksplisit.

Mari kita buat ini sedikit lebih konkret dengan melakukan upaya pertama untuk menerapkan konvensi panjang diskriminator:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

Konvensi ini menerapkan IEntityTypeBaseTypeChangedConvention, yang berarti akan dipicu setiap kali hierarki warisan yang dipetakan untuk jenis entitas diubah. Konvensi kemudian menemukan dan mengonfigurasi properti diskriminator string untuk hierarki.

Konvensi ini kemudian digunakan dengan memanggil Add di ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

Tip

Daripada menambahkan instans konvensi secara langsung, Add metode ini menerima pabrik untuk membuat instans konvensi. Ini memungkinkan konvensi untuk menggunakan dependensi dari penyedia layanan internal EF Core. Karena konvensi ini tidak memiliki dependensi, parameter penyedia layanan diberi nama _, menunjukkan bahwa itu tidak pernah digunakan.

Membangun model dan melihat jenis entitas menunjukkan bahwa ini telah berfungsi --properti diskriminator sekarang dikonfigurasi Post ke dengan panjang maksimum 24:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Tetapi apa yang terjadi jika kita sekarang secara eksplisit mengonfigurasi properti diskriminator yang berbeda? Contohnya:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

Melihat tampilan debug model, kami menemukan bahwa panjang diskriminator tidak lagi dikonfigurasi!

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

Ini karena properti diskriminator yang kami konfigurasi dalam konvensi kami kemudian dihapus ketika diskriminator kustom ditambahkan. Kami dapat mencoba memperbaikinya dengan menerapkan antarmuka lain pada konvensi kami untuk bereaksi terhadap perubahan diskriminator, tetapi mencari tahu antarmuka mana yang akan diterapkan tidak mudah.

Untungnya, ada cara berbeda untuk mendekati ini yang membuat segalanya jauh lebih mudah. Banyak waktu, tidak peduli seperti apa model saat sedang dibangun, selama model akhir benar. Selain itu, konfigurasi yang ingin kita terapkan sering kali tidak perlu memicu konvensi lain untuk bereaksi. Oleh karena itu, konvensi kami dapat menerapkan IModelFinalizingConvention. Konvensi finalisasi model berjalan setelah semua bangunan model lainnya selesai, dan begitu juga memiliki akses ke status akhir model. Konvensi finalisasi model biasanya akan mengulangi seluruh elemen model yang mengonfigurasi model saat berjalan. Jadi, dalam hal ini, kita akan menemukan setiap diskriminator dalam model dan mengonfigurasinya:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

Setelah membangun model dengan konvensi baru ini, kami menemukan bahwa panjang diskriminator sekarang dikonfigurasi dengan benar meskipun telah disesuaikan:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Hanya untuk bersenang-senang, mari kita melangkah lebih jauh dan mengonfigurasi panjang maksimum menjadi panjang nilai diskriminator terpanjang.

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

Sekarang panjang maks kolom diskriminator adalah 8, yang merupakan panjang "Unggulan", nilai diskriminator terpanjang yang digunakan.

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

Tip

Anda mungkin bertanya-tanya apakah konvensi juga harus membuat indeks untuk kolom diskriminator. Ada diskusi tentang hal ini di GitHub. Jawaban singkatnya adalah bahwa kadang-kadang indeks mungkin berguna, tetapi sebagian besar waktu mungkin tidak akan. Oleh karena itu, yang terbaik adalah membuat indeks yang sesuai di sini sesuai kebutuhan, daripada memiliki konvensi untuk melakukannya selalu. Tetapi jika Anda tidak setuju dengan ini, maka konvensi di atas dapat dengan mudah dimodifikasi untuk membuat indeks juga.

Contoh: Panjang default untuk semua properti string

Mari kita lihat contoh lain di mana konvensi finalisasi dapat digunakan--kali ini, mengatur panjang maksimum default untuk properti string apa pun , seperti yang diminta di GitHub. Konvensi ini terlihat sangat mirip dengan contoh sebelumnya:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

Konvensi ini cukup sederhana. Ini menemukan setiap properti string dalam model dan mengatur panjang maksimumnya menjadi 512. Melihat tampilan debug di properti untuk Post, kita melihat bahwa semua properti string sekarang memiliki panjang maksimum 512.

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Content Tetapi properti mungkin harus memungkinkan lebih dari 512 karakter, atau semua posting kami akan sangat pendek! Ini dapat dilakukan tanpa mengubah konvensi kami dengan secara eksplisit mengonfigurasi panjang maksimum hanya untuk properti ini, baik menggunakan atribut pemetaan:

[MaxLength(4000)]
public string Content { get; set; }

Atau dengan kode di OnModelCreating:

modelBuilder.Entity<Post>()
    .Property(post => post.Content)
    .HasMaxLength(4000);

Sekarang semua properti memiliki panjang maksimum 512, kecuali Content yang secara eksplisit dikonfigurasi dengan 4000:

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(4000)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Jadi mengapa konvensi kami tidak mengambil alih panjang maks yang dikonfigurasi secara eksplisit? Jawabannya adalah bahwa EF Core melacak bagaimana setiap bagian konfigurasi dibuat. Ini diwakili oleh ConfigurationSource enum. Jenis konfigurasi yang berbeda adalah:

  • Explicit: Elemen model dikonfigurasi secara eksplisit di OnModelCreating
  • DataAnnotation: Elemen model dikonfigurasi menggunakan atribut pemetaan (alias anotasi data) pada jenis CLR
  • Convention: Elemen model dikonfigurasi oleh konvensi pembuatan model

Konvensi tidak pernah mengambil alih konfigurasi yang ditandai sebagai DataAnnotation atau Explicit. Ini dicapai dengan menggunakan "penyusun konvensi", misalnya, IConventionPropertyBuilder, yang diperoleh dari Builder properti . Contohnya:

property.Builder.HasMaxLength(512);

Panggilan HasMaxLength pada penyusun konvensi hanya akan mengatur panjang maksimum jika belum dikonfigurasi oleh atribut pemetaan atau di OnModelCreating.

Metode penyusun seperti ini juga memiliki parameter kedua: fromDataAnnotation. Atur ini ke true jika konvensi membuat konfigurasi atas nama atribut pemetaan. Contohnya:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

Ini mengatur ConfigurationSource ke DataAnnotation, yang berarti bahwa nilai sekarang dapat ditimpa oleh pemetaan eksplisit pada OnModelCreating, tetapi tidak dengan konvensi atribut non-pemetaan.

Akhirnya, sebelum kita meninggalkan contoh ini, apa yang terjadi jika kita menggunakan dan MaxStringLengthConventionDiscriminatorLengthConvention3 pada saat yang sama? Jawabannya adalah bahwa itu tergantung pada urutan mana mereka ditambahkan, karena model menyelesaikan konvensi berjalan dalam urutan ditambahkan. Jadi jika MaxStringLengthConvention ditambahkan terakhir, maka akan berjalan terakhir, dan akan mengatur panjang maksimum properti diskriminator ke 512. Oleh karena itu, dalam hal ini, lebih baik menambahkan DiscriminatorLengthConvention3 yang terakhir sehingga dapat mengambil alih panjang maks default hanya untuk properti diskriminator, sambil meninggalkan semua properti string lainnya sebagai 512.

Mengganti konvensi yang ada

Terkadang daripada menghapus konvensi yang ada sepenuhnya, kita malah ingin menggantinya dengan konvensi yang pada dasarnya melakukan hal yang sama, tetapi dengan perilaku yang berubah. Ini berguna karena konvensi yang ada sudah akan mengimplementasikan antarmuka yang dibutuhkan sehingga dipicu dengan tepat.

Contoh: Pemetaan properti keikutsertaan

EF Core memetakan semua properti baca-tulis publik berdasarkan konvensi. Ini mungkin tidak sesuai dengan cara jenis entitas Anda ditentukan. Untuk mengubah ini, kita dapat mengganti PropertyDiscoveryConvention dengan implementasi kita sendiri yang tidak memetakan properti apa pun kecuali secara eksplisit dipetakan atau OnModelCreating ditandai dengan atribut baru yang disebut Persist:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

Berikut adalah konvensi baru:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

Tip

Saat mengganti konvensi bawaan, implementasi konvensi baru harus diwarisi dari kelas konvensi yang ada. Perhatikan bahwa beberapa konvensi memiliki implementasi relasional atau khusus penyedia, dalam hal ini implementasi konvensi baru harus mewarisi dari kelas konvensi yang paling spesifik yang ada untuk penyedia database yang digunakan.

Konvensi kemudian didaftarkan menggunakan Replace metode dalam ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

Tip

Ini adalah kasus di mana konvensi yang ada memiliki dependensi, yang diwakili oleh ProviderConventionSetBuilderDependencies objek dependensi. Ini diperoleh dari penyedia layanan internal menggunakan GetRequiredService dan diteruskan ke konstruktor konvensi.

Konvensi ini berfungsi dengan mendapatkan semua properti dan bidang yang dapat dibaca dari jenis entitas tertentu. Jika anggota dikaitkan dengan [Persist], maka anggota dipetakan dengan memanggil:

entityTypeBuilder.Property(memberInfo);

Di sisi lain, jika anggota adalah properti yang seharusnya telah dipetakan, maka itu dikecualikan dari model menggunakan:

entityTypeBuilder.Ignore(propertyInfo.Name);

Perhatikan bahwa konvensi ini memungkinkan bidang dipetakan (selain properti) selama ditandai dengan [Persist]. Ini berarti kita dapat menggunakan bidang privat sebagai kunci tersembunyi dalam model.

Misalnya, pertimbangkan jenis entitas berikut:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

Model yang dibangun dari jenis entitas ini adalah:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

Perhatikan bahwa biasanya, IsClean akan dipetakan, tetapi karena tidak ditandai dengan [Perist] (mungkin karena kebersihan bukan properti cucian yang persisten), sekarang diperlakukan sebagai properti yang tidak dipetakan.

Tip

Konvensi ini tidak dapat diimplementasikan sebagai konvensi finalisasi model karena pemetaan properti memicu banyak konvensi lain untuk dijalankan untuk mengonfigurasi properti yang dipetakan lebih lanjut.

Pemetaan prosedur tersimpan

Secara default, EF Core menghasilkan perintah sisipkan, perbarui, dan hapus yang berfungsi langsung dengan tabel atau tampilan yang dapat diperbarui. EF7 memperkenalkan dukungan untuk pemetaan perintah ini ke prosedur tersimpan.

Tip

EF Core selalu mendukung kueri melalui prosedur tersimpan. Dukungan baru di EF7 secara eksplisit tentang menggunakan prosedur tersimpan untuk penyisipan, pembaruan, dan penghapusan.

Penting

Dukungan untuk pemetaan prosedur tersimpan tidak menyiratkan bahwa prosedur tersimpan direkomendasikan.

Prosedur tersimpan dipetakan dalam OnModelCreating menggunakan InsertUsingStoredProcedure, , UpdateUsingStoredProceduredan DeleteUsingStoredProcedure. Misalnya, untuk memetakan prosedur tersimpan Person untuk jenis entitas:

modelBuilder.Entity<Person>()
    .InsertUsingStoredProcedure(
        "People_Insert",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(a => a.Name);
            storedProcedureBuilder.HasResultColumn(a => a.Id);
        })
    .UpdateUsingStoredProcedure(
        "People_Update",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        })
    .DeleteUsingStoredProcedure(
        "People_Delete",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        });

Konfigurasi ini memetakan ke prosedur tersimpan berikut saat menggunakan SQL Server:

Untuk penyisipan

CREATE PROCEDURE [dbo].[People_Insert]
    @Name [nvarchar](max)
AS
BEGIN
      INSERT INTO [People] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@Name);
END

Untuk pembaruan

CREATE PROCEDURE [dbo].[People_Update]
    @Id [int],
    @Name_Original [nvarchar](max),
    @Name [nvarchar](max)
AS
BEGIN
    UPDATE [People] SET [Name] = @Name
    WHERE [Id] = @Id AND [Name] = @Name_Original
    SELECT @@ROWCOUNT
END

Untuk penghapusan

CREATE PROCEDURE [dbo].[People_Delete]
    @Id [int],
    @Name_Original [nvarchar](max)
AS
BEGIN
    DELETE FROM [People]
    OUTPUT 1
    WHERE [Id] = @Id AND [Name] = @Name_Original;
END

Tip

Prosedur tersimpan tidak perlu digunakan untuk setiap jenis dalam model, atau untuk semua operasi pada jenis tertentu. Misalnya, jika hanya DeleteUsingStoredProcedure ditentukan untuk jenis tertentu, maka EF Core akan menghasilkan SQL seperti biasa untuk operasi penyisipan dan pembaruan dan hanya menggunakan prosedur tersimpan untuk penghapusan.

Argumen pertama yang diteruskan ke setiap metode adalah nama prosedur tersimpan. Ini dapat dihilangkan, dalam hal ini EF Core akan menggunakan nama tabel yang ditambahkan dengan "_Insert", "_Update", atau "_Delete". Jadi, dalam contoh di atas, karena tabel disebut "Orang", nama prosedur tersimpan dapat dihapus tanpa perubahan fungsionalitas.

Argumen kedua adalah penyusun yang digunakan untuk mengonfigurasi input dan output prosedur tersimpan, termasuk parameter, nilai pengembalian, dan kolom hasil.

Parameter

Parameter harus ditambahkan ke penyusun dalam urutan yang sama seperti yang muncul dalam definisi prosedur tersimpan.

Catatan

Parameter dapat dinamai, tetapi EF Core selalu memanggil prosedur tersimpan menggunakan argumen posisional daripada argumen bernama. Pilih Izinkan mengonfigurasi pemetaan sproc untuk menggunakan nama parameter untuk pemanggilan jika memanggil berdasarkan nama adalah sesuatu yang Anda minati.

Argumen pertama untuk setiap metode penyusun parameter menentukan properti dalam model tempat parameter terikat. Ini bisa menjadi ekspresi lambda:

storedProcedureBuilder.HasParameter(a => a.Name);

Atau string, yang sangat berguna saat memetakan properti bayangan:

storedProcedureBuilder.HasParameter("Name");

Parameternya adalah, secara default, dikonfigurasi untuk "input". Parameter "Output" atau "input/output" dapat dikonfigurasi menggunakan penyusun berlapis. Contohnya:

storedProcedureBuilder.HasParameter(
    document => document.RetrievedOn, 
    parameterBuilder => parameterBuilder.IsOutput());

Ada tiga metode penyusun yang berbeda untuk berbagai rasa parameter:

  • HasParameter menentukan parameter normal yang terikat ke nilai properti yang diberikan saat ini.
  • HasOriginalValueParameter menentukan parameter yang terikat ke nilai asli properti yang diberikan. Nilai asli adalah nilai yang dimiliki properti saat dikueri dari database, jika diketahui. Jika nilai ini tidak diketahui, maka nilai saat ini digunakan sebagai gantinya. Parameter nilai asli berguna untuk token konkurensi.
  • HasRowsAffectedParameter menentukan parameter yang digunakan untuk mengembalikan jumlah baris yang terpengaruh oleh prosedur tersimpan.

Tip

Parameter nilai asli harus digunakan untuk nilai kunci dalam prosedur tersimpan "perbarui" dan "hapus". Ini memastikan bahwa baris yang benar akan diperbarui di versi EF Core yang akan datang yang mendukung nilai kunci yang dapat diubah.

Mengembalikan nilai

EF Core mendukung tiga mekanisme untuk mengembalikan nilai dari prosedur tersimpan:

  • Parameter output, seperti yang ditunjukkan di atas.
  • Kolom hasil, yang ditentukan menggunakan metode penyusun HasResultColumn .
  • Nilai pengembalian, yang terbatas pada mengembalikan jumlah baris yang terpengaruh, dan ditentukan menggunakan metode penyusun HasRowsAffectedReturnValue .

Nilai yang dikembalikan dari prosedur tersimpan sering digunakan untuk nilai yang dihasilkan, default, atau dihitung, seperti dari Identity kunci atau kolom komputasi. Misalnya, konfigurasi berikut menentukan empat kolom hasil:

entityTypeBuilder.InsertUsingStoredProcedure(
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(document => document.Title);
            storedProcedureBuilder.HasResultColumn(document => document.Id);
            storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RowVersion);
        });

Ini digunakan untuk mengembalikan:

  • Nilai kunci yang dihasilkan untuk Id properti .
  • Nilai default yang dihasilkan oleh database untuk FirstRecordedOn properti .
  • Nilai komputasi yang dihasilkan oleh database untuk RetrievedOn properti .
  • Token konkurensi yang dihasilkan rowversion secara otomatis untuk RowVersion properti .

Konfigurasi ini memetakan ke prosedur tersimpan berikut saat menggunakan SQL Server:

CREATE PROCEDURE [dbo].[Documents_Insert]
    @Title [nvarchar](max)
AS
BEGIN
    INSERT INTO [Documents] ([Title])
    OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion]
    VALUES (@Title);
END

Konkurensi optimis

Konkurensi optimis bekerja dengan cara yang sama dengan prosedur tersimpan seperti tanpanya. Prosedur tersimpan harus:

  • Gunakan token konkurensi dalam WHERE klausul untuk memastikan bahwa baris hanya diperbarui jika memiliki token yang valid. Nilai yang digunakan untuk token konkurensi biasanya, tetapi tidak harus, nilai asli properti token konkurensi.
  • Mengembalikan jumlah baris yang terpengaruh sehingga EF Core dapat membandingkan ini dengan jumlah baris yang diharapkan yang terpengaruh dan melemparkan DbUpdateConcurrencyException jika nilai tidak cocok.

Misalnya, prosedur tersimpan SQL Server berikut menggunakan rowversion token konkurensi otomatis:

CREATE PROCEDURE [dbo].[Documents_Update]
    @Id [int],
    @RowVersion_Original [rowversion],
    @Title [nvarchar](max),
    @RowVersion [rowversion] OUT
AS
BEGIN
    DECLARE @TempTable table ([RowVersion] varbinary(8));
    UPDATE [Documents] SET
        [Title] = @Title
    OUTPUT INSERTED.[RowVersion] INTO @TempTable
    WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original
    SELECT @@ROWCOUNT;
    SELECT @RowVersion = [RowVersion] FROM @TempTable;
END

Ini dikonfigurasi di EF Core menggunakan:

.UpdateUsingStoredProcedure(
    storedProcedureBuilder =>
    {
        storedProcedureBuilder.HasOriginalValueParameter(document => document.Id);
        storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion);
        storedProcedureBuilder.HasParameter(document => document.Title);
        storedProcedureBuilder.HasParameter(document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput());
        storedProcedureBuilder.HasRowsAffectedResultColumn();
    });

Perhatikan bahwa:

  • Nilai asli token RowVersion konkurensi digunakan.
  • Prosedur tersimpan menggunakan klausul WHERE untuk memastikan bahwa baris hanya diperbarui jika RowVersion nilai asli cocok.
  • Nilai baru yang dihasilkan untuk RowVersion disisipkan ke dalam tabel sementara.
  • Jumlah baris yang terpengaruh (@@ROWCOUNT) dan nilai yang dihasilkan RowVersion dikembalikan.

Memetakan hierarki pewarisan ke prosedur tersimpan

EF Core mengharuskan prosedur tersimpan mengikuti tata letak tabel untuk jenis dalam hierarki. Ini berarti bahwa:

  • Hierarki yang dipetakan menggunakan TPH harus memiliki satu prosedur sisipkan, perbarui, dan/atau hapus tersimpan yang menargetkan satu tabel yang dipetakan. Sisipkan dan perbarui prosedur tersimpan harus memiliki parameter untuk nilai diskriminator.
  • Hierarki yang dipetakan menggunakan TPT harus memiliki prosedur sisipkan, perbarui, dan/atau hapus tersimpan untuk setiap jenis, termasuk jenis abstrak. EF Core akan melakukan beberapa panggilan sesuai kebutuhan untuk memperbarui, menyisipkan, dan menghapus baris di semua tabel.
  • Hierarki yang dipetakan menggunakan TPC harus memiliki prosedur sisipkan, perbarui, dan/atau hapus tersimpan untuk setiap jenis konkret, tetapi bukan jenis abstrak.

Catatan

Jika menggunakan prosedur tersimpan tunggal per jenis konkret terlepas dari strategi pemetaan adalah sesuatu yang Anda minati, maka pilih Dukungan menggunakan satu sproc per jenis konkret terlepas dari strategi pemetaan warisan.

Memetakan jenis yang dimiliki ke prosedur tersimpan

Konfigurasi prosedur tersimpan untuk jenis yang dimiliki dilakukan di penyusun jenis milik berlapis. Contoh:

modelBuilder.Entity<Person>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.OwnsOne(
            author => author.Contact,
            ownedNavigationBuilder =>
            {
                ownedNavigationBuilder.ToTable("Contacts");
                ownedNavigationBuilder
                    .InsertUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                        })
                    .UpdateUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
                        })
                    .DeleteUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
            });
    });

Catatan

Prosedur yang disimpan saat ini untuk menyisipkan, memperbarui, dan menghapus hanya jenis yang dimiliki dukungan harus dipetakan ke tabel terpisah. Artinya, jenis yang dimiliki tidak dapat diwakili oleh kolom dalam tabel pemilik. Pilih Tambahkan dukungan pemisahan "tabel" ke pemetaan sproc CUD jika ini adalah batasan yang ingin Anda lihat dihapus.

Memetakan entitas gabungan banyak ke banyak ke prosedur tersimpan

Konfigurasi prosedur tersimpan entitas gabungan banyak ke banyak dapat dilakukan sebagai bagian dari konfigurasi banyak-ke-banyak. Contohnya:

modelBuilder.Entity<Book>(
    entityTypeBuilder =>
    {
        entityTypeBuilder
            .HasMany(document => document.Authors)
            .WithMany(author => author.PublishedWorks)
            .UsingEntity<Dictionary<string, object>>(
                "BookPerson",
                builder => builder.HasOne<Person>().WithMany().OnDelete(DeleteBehavior.Cascade),
                builder => builder.HasOne<Book>().WithMany().OnDelete(DeleteBehavior.ClientCascade),
                joinTypeBuilder =>
                {
                    joinTypeBuilder
                        .InsertUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasParameter("AuthorsId");
                                storedProcedureBuilder.HasParameter("PublishedWorksId");
                            })
                        .DeleteUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasOriginalValueParameter("AuthorsId");
                                storedProcedureBuilder.HasOriginalValueParameter("PublishedWorksId");
                                storedProcedureBuilder.HasRowsAffectedResultColumn();
                            });
                });
    });

Pencegat dan peristiwa baru dan yang ditingkatkan

Pencegat EF Core memungkinkan intersepsi, modifikasi, dan/atau penindasan operasi EF Core. EF Core juga mencakup peristiwadan pengelogan .NET tradisional.

EF7 mencakup penyempurnaan berikut untuk pencegat:

Selain itu, EF7 menyertakan peristiwa .NET tradisional baru untuk:

Bagian berikut menunjukkan beberapa contoh penggunaan kemampuan intersepsi baru ini.

Tindakan sederhana pada pembuatan entitas

Tip

Kode yang ditampilkan di sini berasal dari SimpleMaterializationSample.cs.

Yang baru IMaterializationInterceptor mendukung intersepsi sebelum dan sesudah instans entitas dibuat, dan sebelum dan sesudah properti instans tersebut diinisialisasi. Pencegat dapat mengubah atau mengganti instans entitas di setiap titik. Ini memungkinkan:

  • Mengatur properti yang tidak dipetakan atau metode panggilan yang diperlukan untuk validasi, nilai komputasi, atau bendera.
  • Menggunakan pabrik untuk membuat instans.
  • Membuat instans entitas yang berbeda dari yang biasanya dibuat EF, seperti instans dari cache, atau jenis proksi.
  • Menyuntikkan layanan ke dalam instans entitas.

Misalnya, bayangkan bahwa kita ingin melacak waktu saat entitas diambil dari database, mungkin sehingga dapat ditampilkan kepada pengguna yang mengedit data. Untuk mencapai hal ini, pertama-tama kita mendefinisikan antarmuka:

public interface IHasRetrieved
{
    DateTime Retrieved { get; set; }
}

Menggunakan antarmuka umum dengan pencegat karena memungkinkan pencegat yang sama untuk bekerja dengan banyak jenis entitas yang berbeda. Contohnya:

public class Customer : IHasRetrieved
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? PhoneNumber { get; set; }

    [NotMapped]
    public DateTime Retrieved { get; set; }
}

Perhatikan bahwa [NotMapped] atribut digunakan untuk menunjukkan bahwa properti ini hanya digunakan saat bekerja dengan entitas, dan tidak boleh bertahan pada database.

Pencegat kemudian harus menerapkan metode yang sesuai dari IMaterializationInterceptor dan mengatur waktu yang diambil:

public class SetRetrievedInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasRetrieved hasRetrieved)
        {
            hasRetrieved.Retrieved = DateTime.UtcNow;
        }

        return instance;
    }
}

Instans pencegat ini terdaftar saat mengonfigurasi DbContext:

public class CustomerContext : DbContext
{
    private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();

    public DbSet<Customer> Customers
        => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_setRetrievedInterceptor)
            .UseSqlite("Data Source = customers.db");
}

Tip

Pencegat ini tanpa status, yang umum, sehingga satu instans dibuat dan dibagikan di antara semua DbContext instans.

Sekarang, setiap kali dikueri Customer dari database, Retrieved properti akan diatur secara otomatis. Contohnya:

await using (var context = new CustomerContext())
{
    var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
    Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}

Menghasilkan output:

Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'

Menyuntikkan layanan ke dalam entitas

Tip

Kode yang ditampilkan di sini berasal dari InjectLoggerSample.cs.

EF Core sudah memiliki dukungan bawaan untuk menyuntikkan beberapa layanan khusus ke dalam instans konteks; misalnya, lihat Pemuatan malas tanpa proksi, yang berfungsi dengan menyuntikkan ILazyLoader layanan.

Dapat IMaterializationInterceptor digunakan untuk menggeneralisasi ini ke layanan apa pun. Contoh berikut menunjukkan cara menyuntikkan ILogger ke entitas sehingga mereka dapat melakukan pengelogan mereka sendiri.

Catatan

Menyuntikkan layanan ke entitas menggandeng jenis entitas tersebut ke layanan yang disuntikkan, yang dianggap beberapa orang sebagai anti-pola.

Seperti sebelumnya, antarmuka digunakan untuk menentukan apa yang dapat dilakukan.

public interface IHasLogger
{
    ILogger? Logger { get; set; }
}

Dan jenis entitas yang akan mencatat harus mengimplementasikan antarmuka ini. Contohnya:

public class Customer : IHasLogger
{
    private string? _phoneNumber;

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public string? PhoneNumber
    {
        get => _phoneNumber;
        set
        {
            Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

            _phoneNumber = value;
        }
    }

    [NotMapped]
    public ILogger? Logger { get; set; }
}

Kali ini, pencegat harus menerapkan IMaterializationInterceptor.InitializedInstance, yang dipanggil setelah setiap instans entitas dibuat dan nilai propertinya telah diinisialisasi. Pencegat memperoleh ILogger dari konteks dan menginisialisasi IHasLogger.Logger dengannya:

public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
    private ILogger? _logger;

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasLogger hasLogger)
        {
            _logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
            hasLogger.Logger = _logger;
        }

        return instance;
    }
}

Kali ini instans baru pencegat digunakan untuk setiap DbContext instans, karena ILogger yang diperoleh dapat berubah per DbContext instans, dan ILogger di-cache pada pencegat:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());

Sekarang, setiap kali Customer.PhoneNumber diubah, perubahan ini akan dicatat ke log aplikasi. Contohnya:

info: CustomersLogger[1]
      Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.

Penyadapan pohon ekspresi LINQ

Tip

Kode yang diperlihatkan di sini berasal dari QueryInterceptionSample.cs.

EF Core menggunakan kueri .NET LINQ. Ini biasanya melibatkan penggunaan pengkompilasi C#, VB, atau F# untuk membangun pohon ekspresi yang kemudian diterjemahkan oleh EF Core ke dalam SQL yang sesuai. Misalnya, pertimbangkan metode yang mengembalikan halaman pelanggan:

Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .Skip(page * 20).Take(20).ToListAsync();
}

Tip

Kueri ini menggunakan EF.Property metode untuk menentukan properti yang akan diurutkan. Ini memungkinkan aplikasi untuk secara dinamis meneruskan nama properti, memungkinkan pengurutan berdasarkan properti apa pun dari jenis entitas. Ketahuilah bahwa pengurutan menurut kolom yang tidak diindeks bisa lambat.

Ini akan berfungsi dengan baik selama properti yang digunakan untuk pengurutan selalu mengembalikan urutan yang stabil. Tapi ini mungkin tidak selalu terjadi. Misalnya, kueri LINQ di atas menghasilkan yang berikut ini di SQLite saat mengurutkan dengan Customer.City:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0

Jika ada beberapa pelanggan dengan yang sama City, maka pemesanan kueri ini tidak stabil. Ini dapat menyebabkan hasil yang hilang atau duplikat sebagai halaman pengguna melalui data.

Cara umum untuk memperbaiki masalah ini adalah dengan melakukan pengurutan sekunder berdasarkan kunci primer. Namun, daripada menambahkan ini secara manual ke setiap kueri, EF7 memungkinkan penyadapan pohon ekspresi kueri di mana pengurutan sekunder dapat ditambahkan secara dinamis. Untuk memfasilitasi hal ini, kami akan kembali menggunakan antarmuka, kali ini untuk entitas apa pun yang memiliki kunci primer bilangan bulat:

public interface IHasIntKey
{
    int Id { get; }
}

Antarmuka ini diimplementasikan oleh jenis entitas yang menarik:

public class Customer : IHasIntKey
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? City { get; set; }
    public string? PhoneNumber { get; set; }
}

Kita kemudian membutuhkan pencegat yang mengimplementasikan IQueryExpressionInterceptor

public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
        => new KeyOrderingExpressionVisitor().Visit(queryExpression);

    private class KeyOrderingExpressionVisitor : ExpressionVisitor
    {
        private static readonly MethodInfo ThenByMethod
            = typeof(Queryable).GetMethods()
                .Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);

        protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
        {
            var methodInfo = methodCallExpression!.Method;
            if (methodInfo.DeclaringType == typeof(Queryable)
                && methodInfo.Name == nameof(Queryable.OrderBy)
                && methodInfo.GetParameters().Length == 2)
            {
                var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
                if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
                {
                    var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
                    var entityParameterExpression = lambdaExpression.Parameters[0];

                    return Expression.Call(
                        ThenByMethod.MakeGenericMethod(
                            sourceType,
                            typeof(int)),
                        base.VisitMethodCall(methodCallExpression),
                        Expression.Lambda(
                            typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
                            Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
                            entityParameterExpression));
                }
            }

            return base.VisitMethodCall(methodCallExpression);
        }
    }
}

Ini mungkin terlihat cukup rumit - dan itu! Bekerja dengan pohon ekspresi biasanya tidak mudah. Mari kita lihat apa yang terjadi:

  • Pada dasarnya, pencegat merangkum ExpressionVisitor. Pengunjung mengambil alih VisitMethodCall, yang akan dipanggil setiap kali ada panggilan ke metode di pohon ekspresi kueri.

  • Pengunjung memeriksa apakah ini adalah panggilan ke OrderBy metode yang kami minati atau tidak.

  • Jika ya, maka pengunjung lebih lanjut memeriksa apakah panggilan metode generik adalah untuk jenis yang mengimplementasikan antarmuka kami IHasIntKey .

  • Pada titik ini kita tahu bahwa panggilan metode adalah dari formulir OrderBy(e => ...). Kami mengekstrak ekspresi lambda dari panggilan ini dan mendapatkan parameter yang digunakan dalam ekspresi itu --yaitu, e.

  • Kami sekarang membangun yang baru MethodCallExpression menggunakan metode penyusun Expression.Call . Dalam hal ini, metode yang dipanggil adalah ThenBy(e => e.Id). Kami membangun ini menggunakan parameter yang diekstrak di atas dan akses properti ke Id properti IHasIntKey antarmuka.

  • Input ke dalam panggilan ini adalah asli OrderBy(e => ...), sehingga hasil akhirnya adalah ekspresi untuk OrderBy(e => ...).ThenBy(e => e.Id).

  • Ekspresi yang dimodifikasi ini dikembalikan dari pengunjung, yang berarti kueri LINQ sekarang telah dimodifikasi dengan tepat untuk menyertakan ThenBy panggilan.

  • EF Core melanjutkan dan mengkompilasi ekspresi kueri ini ke dalam SQL yang sesuai untuk database yang digunakan.

Pencegat ini terdaftar dengan cara yang sama seperti yang kami lakukan untuk contoh pertama. GetPageOfCustomers Menjalankan sekarang menghasilkan SQL berikut:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0

Ini sekarang akan selalu menghasilkan pemesanan yang stabil, bahkan jika ada beberapa pelanggan dengan yang sama City.

Fiuh! Itu banyak kode untuk membuat perubahan sederhana pada kueri. Dan yang lebih buruk lagi, bahkan mungkin tidak berfungsi untuk semua kueri. Sangat sulit untuk menulis pengunjung ekspresi yang mengenali semua bentuk kueri yang seharusnya, dan tidak satu pun dari yang seharusnya tidak. Misalnya, ini kemungkinan tidak akan berfungsi jika pengurutan dilakukan dalam subkueri.

Ini membawa kita ke titik kritis tentang pencegat --selalu tanyakan pada diri sendiri apakah ada cara yang lebih mudah untuk melakukan apa yang Anda inginkan. Pencegat itu kuat, tetapi mudah untuk mendapatkan hal-hal yang salah. Mereka, seperti yang dikatakan, cara mudah untuk menembak diri sendiri di kaki.

Misalnya, bayangkan jika kita malah mengubah metode kita GetPageOfCustomers seperti itu:

Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .ThenBy(e => e.Id)
        .Skip(page * 20).Take(20).ToListAsync();
}

Dalam hal ThenBy ini, hanya ditambahkan ke kueri. Ya, mungkin perlu dilakukan secara terpisah untuk setiap kueri, tetapi sederhana, mudah dipahami, dan akan selalu berfungsi.

Intersepsi konkurensi optimis

Tip

Kode yang ditampilkan di sini berasal dari OptimisticConcurrencyInterceptionSample.cs.

EF Core mendukung pola konkurensi optimis dengan memeriksa bahwa jumlah baris yang benar-benar terpengaruh oleh pembaruan atau penghapusan sama dengan jumlah baris yang diharapkan terpengaruh. Ini sering digabungkan dengan token konkurensi; artinya, nilai kolom yang hanya akan cocok dengan nilai yang diharapkan jika baris belum diperbarui sejak nilai yang diharapkan dibaca.

EF menandakan pelanggaran konkurensi optimis dengan melempar DbUpdateConcurrencyException. Di EF7, ISaveChangesInterceptor memiliki metode ThrowingConcurrencyException baru dan ThrowingConcurrencyExceptionAsync yang dipanggil sebelum DbUpdateConcurrencyException dilemparkan. Titik penyadapan ini memungkinkan pengecualian ditekan, mungkin ditambah dengan perubahan database asinkron untuk menyelesaikan pelanggaran.

Misalnya, jika dua permintaan mencoba menghapus entitas yang sama pada saat yang hampir bersamaan, maka penghapusan kedua mungkin gagal karena baris dalam database tidak ada lagi. Ini mungkin baik--hasil akhirnya adalah bahwa entitas telah dihapus. Pencegat berikut menunjukkan bagaimana hal ini dapat dilakukan:

public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
    public InterceptionResult ThrowingConcurrencyException(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result)
    {
        if (eventData.Entries.All(e => e.State == EntityState.Deleted))
        {
            Console.WriteLine("Suppressing Concurrency violation for command:");
            Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

            return InterceptionResult.Suppress();
        }

        return result;
    }

    public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
        => new(ThrowingConcurrencyException(eventData, result));
}

Ada beberapa hal yang perlu diperhatikan tentang pencegat ini:

  • Metode intersepsi sinkron dan asinkron diimplementasikan. Ini penting jika aplikasi dapat memanggil atau SaveChangesSaveChangesAsync. Namun, jika semua kode aplikasi asinkron, maka hanya ThrowingConcurrencyExceptionAsync perlu diimplementasikan. Demikian juga, jika aplikasi tidak pernah menggunakan metode database asinkron, maka hanya ThrowingConcurrencyException perlu diimplementasikan. Ini umumnya berlaku untuk semua pencegat dengan metode sinkronisasi dan asinkron. (Mungkin ada baiknya menerapkan metode yang tidak digunakan aplikasi Anda untuk melemparkan, untuk berjaga-jaga jika beberapa kode sinkronisasi/asinkron merayap masuk.)
  • Pencegat memiliki akses ke EntityEntry objek untuk entitas yang disimpan. Dalam hal ini, ini digunakan untuk memeriksa apakah pelanggaran konkurensi terjadi atau tidak untuk operasi penghapusan.
  • Jika aplikasi menggunakan penyedia database relasional, maka objek dapat dilemparkan ConcurrencyExceptionEventData ke RelationalConcurrencyExceptionEventData objek. Ini menyediakan informasi tambahan khusus relasional tentang operasi database yang sedang dilakukan. Dalam hal ini, teks perintah relasional dicetak ke konsol.
  • Mengembalikan InterceptionResult.Suppress() memberi tahu EF Core untuk menekan tindakan yang akan diambil --dalam hal ini, melemparkan DbUpdateConcurrencyException. Kemampuan ini untuk mengubah perilaku EF Core, daripada hanya mengamati apa yang dilakukan EF Core, adalah salah satu fitur pencegat yang paling kuat.

Inisialisasi malas string koneksi

Tip

Kode yang ditampilkan di sini berasal dari Lazy Koneksi ionStringSample.cs.

string Koneksi ion seringkali merupakan aset statis yang dibaca dari file konfigurasi. Ini dapat dengan mudah diteruskan ke UseSqlServer atau serupa saat mengonfigurasi DbContext. Namun, terkadang string koneksi dapat berubah untuk setiap instans konteks. Misalnya, setiap penyewa dalam sistem multi-penyewa mungkin memiliki string koneksi yang berbeda.

EF7 memudahkan untuk menangani koneksi dinamis dan string koneksi melalui peningkatan pada IDbConnectionInterceptor. Ini dimulai dengan kemampuan untuk mengonfigurasi DbContext tanpa string koneksi. Contohnya:

services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer());

Salah IDbConnectionInterceptor satu metode kemudian dapat diimplementasikan untuk mengonfigurasi koneksi sebelum digunakan. ConnectionOpeningAsyncadalah pilihan yang baik, karena dapat melakukan operasi asinkron untuk mendapatkan string koneksi, menemukan token akses, dan sebagainya. Misalnya, bayangkan layanan yang dilingkup ke permintaan saat ini yang memahami penyewa saat ini:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Peringatan

Melakukan pencarian asinkron untuk string koneksi, token akses, atau serupa setiap kali diperlukan bisa sangat lambat. Pertimbangkan untuk menyimpan cache hal-hal ini dan hanya menyegarkan string atau token yang di-cache secara berkala. Misalnya, token akses sering dapat digunakan untuk jangka waktu yang signifikan sebelum perlu di-refresh.

Ini dapat disuntikkan ke setiap DbContext instans menggunakan injeksi konstruktor:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public CustomerContext(
        DbContextOptions<CustomerContext> options,
        ITenantConnectionStringFactory connectionStringFactory)
        : base(options)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    // ...
}

Layanan ini kemudian digunakan saat membangun implementasi pencegat untuk konteks:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(
        new ConnectionStringInitializationInterceptor(_connectionStringFactory));

Akhirnya, pencegat menggunakan layanan ini untuk mendapatkan string koneksi secara asinkron dan mengaturnya pertama kali koneksi digunakan:

public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
    private readonly IClientConnectionStringFactory _connectionStringFactory;

    public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new NotSupportedException("Synchronous connections not supported.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
        CancellationToken cancellationToken = new())
    {
        if (string.IsNullOrEmpty(connection.ConnectionString))
        {
            connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
        }

        return result;
    }
}

Catatan

string koneksi hanya diperoleh saat pertama kali koneksi digunakan. Setelah itu, string koneksi yang disimpan di DbConnection akan digunakan tanpa mencari string koneksi baru.

Tip

Pencegat ini mengambil alih metode non-asinkron ConnectionOpening untuk dilemparkan karena layanan untuk mendapatkan string koneksi harus dipanggil dari jalur kode asinkron.

Mencatat statistik kueri SQL Server

Tip

Kode yang diperlihatkan di sini berasal dari QueryStatisticsLoggerSample.cs.

Terakhir, mari kita buat dua pencegat yang bekerja sama untuk mengirim statistik kueri SQL Server ke log aplikasi. Untuk menghasilkan statistik, kita perlu IDbCommandInterceptor melakukan dua hal.

Pertama, pencegat akan mengawali perintah dengan SET STATISTICS IO ON, yang memberi tahu SQL Server untuk mengirim statistik ke klien setelah kumpulan hasil dikonsumsi:

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;

    return new(result);
}

Kedua, pencegat akan menerapkan metode baru DataReaderClosingAsync , yang dipanggil setelah DbDataReader selesai mengkonsumsi hasil, tetapi sebelum ditutup. Ketika SQL Server mengirim statistik, SQL Server menempatkannya dalam hasil kedua pada pembaca, jadi pada titik ini pencegat membaca hasil tersebut dengan memanggil NextResultAsync yang mengisi statistik ke koneksi.

public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
    DbCommand command,
    DataReaderClosingEventData eventData,
    InterceptionResult result)
{
    await eventData.DataReader.NextResultAsync();

    return result;
}

Pencegat kedua diperlukan untuk mendapatkan statistik dari koneksi dan menuliskannya ke pencatat aplikasi. Untuk ini, kita akan menggunakan IDbConnectionInterceptor, menerapkan metode baru ConnectionCreated . ConnectionCreated dipanggil segera setelah EF Core membuat koneksi, sehingga dapat digunakan untuk melakukan konfigurasi tambahan koneksi tersebut. Dalam hal ini, pencegat mendapatkan ILogger dan kemudian menghubungkan ke SqlConnection.InfoMessage dalam peristiwa untuk mencatat pesan.

public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
    var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
    ((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
    {
        logger.LogInformation(1, args.Message);
    };
    return result;
}

Penting

Metode ConnectionCreating dan ConnectionCreated hanya dipanggil ketika EF Core membuat DbConnection. Mereka tidak akan dipanggil jika aplikasi membuat DbConnection dan meneruskannya ke EF Core.

Menjalankan beberapa kode yang menggunakan pencegat ini memperlihatkan statistik kueri SQL Server dalam log:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Customers] USING (
      VALUES (@p0, @p1, 0),
      (@p2, @p3, 1)) AS i ([Name], [PhoneNumber], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name], [PhoneNumber])
      VALUES (i.[Name], i.[PhoneNumber])
      OUTPUT INSERTED.[Id], i._Position;
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 0, logical reads 5, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SELECT TOP(2) [c].[Id], [c].[Name], [c].[PhoneNumber]
      FROM [Customers] AS [c]
      WHERE [c].[Name] = N'Alice'
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

Penyempurnaan kueri

EF7 berisi banyak peningkatan dalam terjemahan kueri LINQ.

GroupBy sebagai operator akhir

Tip

Kode yang ditampilkan di sini berasal dari GroupByFinalOperatorSample.cs.

EF7 mendukung penggunaan GroupBy sebagai operator akhir dalam kueri. Misalnya, kueri LINQ ini:

var query = context.Books.GroupBy(s => s.Price);

Diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

Catatan

Jenis GroupBy ini tidak diterjemahkan langsung ke SQL, sehingga EF Core melakukan pengelompokan pada hasil yang dikembalikan. Namun, ini tidak mengakibatkan data tambahan ditransfer dari server.

GroupJoin sebagai operator akhir

Tip

Kode yang ditampilkan di sini berasal dari GroupJoinFinalOperatorSample.cs.

EF7 mendukung penggunaan GroupJoin sebagai operator akhir dalam kueri. Misalnya, kueri LINQ ini:

var query = context.Customers.GroupJoin(
    context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });

Diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId]
FROM [Customers] AS [c]
OUTER APPLY (
    SELECT [o].[Id], [o].[Amount], [o].[CustomerId]
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId]
) AS [t]
ORDER BY [c].[Id]

Jenis entitas GroupBy

Tip

Kode yang ditampilkan di sini berasal dari GroupByEntityTypeSample.cs.

EF7 mendukung pengelompokan menurut jenis entitas. Misalnya, kueri LINQ ini:

var query = context.Books
    .GroupBy(s => s.Author)
    .Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });

Diterjemahkan ke SQL berikut saat menggunakan SQLite:

SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice]
FROM [Books] AS [b]
INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

Perlu diingat bahwa pengelompokan menurut properti unik, seperti kunci utama, akan selalu lebih efisien daripada pengelompokan menurut jenis entitas. Namun, pengelompokan menurut jenis entitas dapat digunakan untuk jenis entitas kunci dan tanpa kunci.

Selain itu, pengelompokan menurut jenis entitas dengan kunci utama akan selalu menghasilkan satu grup per instans entitas, karena setiap entitas harus memiliki nilai kunci yang unik. Terkadang ada baiknya mengalihkan sumber kueri sehingga pengelompokan tidak diperlukan. Misalnya, kueri berikut mengembalikan hasil yang sama dengan kueri sebelumnya:

var query = context.Authors
    .Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQLite:

SELECT [a].[Id], [a].[Name], (
    SELECT MAX([b].[Price])
    FROM [Books] AS [b]
    WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]

Subkueri tidak mereferensikan kolom yang dikelompokan dari kueri luar

Tip

Kode yang ditampilkan di sini berasal dari UngroupedColumnsQuerySample.cs.

Dalam EF Core 6.0, GROUP BY klausul akan mereferensikan kolom di kueri luar, yang gagal dengan beberapa database dan tidak efisien di yang lain. Misalnya, pertimbangkan kueri di bawah ini:

var query = from s in (from i in context.Invoices
                       group i by i.History.Month
                       into g
                       select new { Month = g.Key, Total = g.Sum(p => p.Amount), })
            select new
            {
                s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount)
            };

Di EF Core 6.0 di SQL Server, ini diterjemahkan ke:

SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment]
FROM [Invoices] AS [i]
GROUP BY DATEPART(month, [i].[History])

Pada EF7, terjemahannya adalah:

SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment]
FROM (
    SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key]
    FROM [Invoices] AS [i]
) AS [t]
GROUP BY [t].[Key]

Koleksi baca-saja dapat digunakan untuk Contains

Tip

Kode yang ditampilkan di sini berasal dari ReadOnlySetQuerySample.cs.

EF7 mendukung penggunaan Contains saat item yang akan dicari terkandung dalam IReadOnlySet atau IReadOnlyCollection, atau IReadOnlyList. Misalnya, kueri LINQ ini:

IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));

Diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5))

Terjemahan untuk fungsi agregat

EF7 memperkenalkan ekstensibilitas yang lebih baik bagi penyedia untuk menerjemahkan fungsi agregat. Pekerjaan ini dan pekerjaan lain di area ini telah menghasilkan beberapa terjemahan baru di seluruh penyedia, termasuk:

Catatan

Fungsi agregat yang bertindak berdasarkan IEnumerable argumen biasanya hanya diterjemahkan dalam GroupBy kueri. Pilih untuk jenis spasial Dukungan di kolom JSON jika Anda tertarik untuk menghapus batasan ini.

Fungsi agregat string

Tip

Kode yang ditampilkan di sini berasal dari StringAggregateFunctionsSample.cs.

Kueri yang menggunakan Join dan Concat sekarang diterjemahkan jika sesuai. Contohnya:

var query = context.Posts
    .GroupBy(post => post.Author)
    .Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });

Kueri ini diterjemahkan ke yang berikut ini saat menggunakan SQL Server:

SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

Ketika dikombinasikan dengan fungsi string lainnya, terjemahan ini memungkinkan beberapa manipulasi string yang kompleks di server. Contohnya:

var query = context.Posts
    .GroupBy(post => post.Author!.Name)
    .Select(
        grouping =>
            new
            {
                PostAuthor = grouping.Key,
                Blogs = string.Concat(
                    grouping
                        .Select(post => post.Blog.Name)
                        .Distinct()
                        .Select(postName => "'" + postName + "' ")),
                ContentSummaries = string.Join(
                    " | ",
                    grouping
                        .Where(post => post.Content.Length >= 10)
                        .Select(post => "'" + post.Content.Substring(0, 10) + "' "))
            });

Kueri ini diterjemahkan ke yang berikut ini saat menggunakan SQL Server:

SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c]
FROM (
    SELECT [a].[Name], COALESCE(STRING_AGG(CASE
        WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'')
    END, N' | '), N'') AS [c]
    FROM [Posts] AS [p]
    LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
    GROUP BY [a].[Name]
) AS [t]
OUTER APPLY (
    SELECT DISTINCT [b].[Name]
    FROM [Posts] AS [p0]
    LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id]
    INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id]
    WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL)
) AS [t0]
ORDER BY [t].[Name]

Fungsi agregat spasial

Tip

Kode yang ditampilkan di sini berasal dari SpatialAggregateFunctionsSample.cs.

Sekarang dimungkinkan bagi penyedia database yang mendukung NetTopologySuite untuk menerjemahkan fungsi agregat spasial berikut:

Tip

Terjemahan ini telah diterapkan oleh tim untuk SQL Server dan SQLite. Untuk penyedia lain, hubungi penyedia layanan untuk menambahkan dukungan jika telah diterapkan untuk penyedia tersebut.

Contohnya:

var query = context.Caches
    .Where(cache => cache.Location.X < -90)
    .GroupBy(cache => cache.Owner)
    .Select(
        grouping => new { Id = grouping.Key, Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) });

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined]
FROM [Caches] AS [c]
WHERE [c].[Location].Long < -90.0E0
GROUP BY [c].[Owner]

Fungsi agregat statistik

Tip

Kode yang ditampilkan di sini berasal dari StatisticalAggregateFunctionsSample.cs.

Terjemahan SQL Server telah diimplementasikan untuk fungsi statistik berikut:

Tip

Terjemahan ini telah diterapkan oleh tim untuk SQL Server. Untuk penyedia lain, hubungi penyedia layanan untuk menambahkan dukungan jika telah diterapkan untuk penyedia tersebut.

Contohnya:

var query = context.Downloads
    .GroupBy(download => download.Uploader.Id)
    .Select(
        grouping => new
        {
            Author = grouping.Key,
            TotalCost = grouping.Sum(d => d.DownloadCount),
            AverageViews = grouping.Average(d => d.DownloadCount),
            VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)),
            VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)),
            StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)),
            StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount))
        });

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample]
FROM [Downloads] AS [d]
INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id]
GROUP BY [u].[Id]

Terjemahan dari string.IndexOf

Tip

Kode yang ditampilkan di sini berasal dari MiscellaneousTranslationsSample.cs.

EF7 sekarang diterjemahkan String.IndexOf dalam kueri LINQ. Contohnya:

var query = context.Posts
    .Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
    .Where(post => post.IndexOfEntity > 0);

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity]
FROM [Posts] AS [p]
WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0

Terjemahan GetType untuk jenis entitas

Tip

Kode yang ditampilkan di sini berasal dari MiscellaneousTranslationsSample.cs.

EF7 sekarang diterjemahkan Object.GetType() dalam kueri LINQ. Contohnya:

var query = context.Posts.Where(post => post.GetType() == typeof(Post));

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQL Server dengan pewarisan TPH:

SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'

Perhatikan bahwa kueri ini hanya Post mengembalikan instans yang sebenarnya berjenis Post, dan bukan jenis turunan apa pun. Ini berbeda dari kueri yang menggunakan is atau OfType, yang juga akan mengembalikan instans dari jenis turunan apa pun. Misalnya, pertimbangkan kueri:

var query = context.Posts.OfType<Post>();

Yang diterjemahkan ke SQL yang berbeda:

      SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
      FROM [Posts] AS [p]

Dan akan mengembalikan entitas dan FeaturedPost .Post

Dukungan untuk AT TIME ZONE

Tip

Kode yang ditampilkan di sini berasal dari MiscellaneousTranslationsSample.cs.

EF7 memperkenalkan fungsi baru AtTimeZone untuk DateTime dan DateTimeOffset. Fungsi-fungsi ini diterjemahkan ke AT TIME ZONE klausul dalam SQL yang dihasilkan. Contohnya:

var query = context.Posts
    .Select(
        post => new
        {
            post.Title,
            PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"),
            UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"),
        });

Kueri ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime]
FROM [Posts] AS [p]

Tip

Terjemahan ini telah diterapkan oleh tim untuk SQL Server. Untuk penyedia lain, hubungi penyedia layanan untuk menambahkan dukungan jika telah diterapkan untuk penyedia tersebut.

Difilter Sertakan pada navigasi tersembunyi

Tip

Kode yang ditampilkan di sini berasal dari MiscellaneousTranslationsSample.cs.

Metode Sertakan sekarang dapat digunakan dengan EF.Property. Ini memungkinkan pemfilteran dan pengurutan bahkan untuk properti navigasi privat, atau navigasi privat yang diwakili oleh bidang. Contohnya:

var query = context.Blogs.Include(
    blog => EF.Property<ICollection<Post>>(blog, "Posts")
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Tindakan ini setara dengan:

var query = context.Blogs.Include(
    blog => Posts
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Tetapi tidak perlu Blog.Posts dapat diakses secara publik.

Saat menggunakan SQL Server, kedua kueri di atas diterjemahkan ke:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
    FROM [Posts] AS [p]
    WHERE [p].[Content] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Title]

Terjemahan cosmos untuk Regex.IsMatch

Tip

Kode yang ditampilkan di sini berasal dari CosmosQueriesSample.cs.

EF7 mendukung penggunaan Regex.IsMatch dalam kueri LINQ terhadap Azure Cosmos DB. Contohnya:

var containsInnerT = await context.Triangles
    .Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
    .ToListAsync();

Diterjemahkan ke SQL berikut:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))

API DbContext dan penyempurnaan perilaku

EF7 berisi berbagai peningkatan kecil pada DbContext dan kelas terkait.

Tip

Kode untuk sampel di bagian ini berasal dari DbContextApiSample.cs.

Suppressor untuk properti DbSet yang tidak diinisialisasi

Properti publik yang dapat DbSet diatur pada secara otomatis diinisialisasi oleh EF Core saat DbContextDbContext dibangun. Misalnya, pertimbangkan definisi berikut DbContext :

public class SomeDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
}

Properti Blogs akan diatur ke DbSet<Blog> instans sebagai bagian dari pembuatan DbContext instans. Ini memungkinkan konteks digunakan untuk kueri tanpa langkah tambahan.

Namun, setelah pengenalan jenis referensi C# nullable, pengompilasi sekarang memperingatkan bahwa properti Blogs yang tidak dapat diubah ke null tidak diinisialisasi:

[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Ini adalah peringatan besar; properti diatur ke nilai non-null oleh EF Core. Juga, menyatakan properti sebagai nullable akan membuat peringatan hilang, tetapi ini bukan ide yang baik karena, secara konseptual, properti tidak dapat diubah ke null dan tidak akan pernah null.

EF7 berisi DiagnosticSuppressor untuk DbSet properti yang DbContext menghentikan pengompilasi menghasilkan peringatan ini.

Tip

Pola ini berasal pada hari-hari ketika properti otomatis C# sangat terbatas. Dengan C#modern, pertimbangkan untuk membuat properti otomatis baca-saja, lalu inisialisasi secara eksplisit di DbContext konstruktor, atau dapatkan instans cache DbSet dari konteks saat diperlukan. Contohnya,public DbSet<Blog> Blogs => Set<Blog>().

Membedakan pembatalan dari kegagalan dalam log

Terkadang aplikasi akan secara eksplisit membatalkan kueri atau operasi database lainnya. Ini biasanya dilakukan menggunakan metode yang CancellationToken diteruskan ke metode yang melakukan operasi.

Di EF Core 6, peristiwa yang dicatat ketika operasi dibatalkan sama dengan yang dicatat ketika operasi gagal karena beberapa alasan lain. EF7 memperkenalkan peristiwa log baru khusus untuk operasi database yang dibatalkan. Peristiwa baru ini, secara default, dicatat di Debug tingkat . Tabel berikut ini memperlihatkan peristiwa yang relevan dan tingkat log defaultnya:

Event Deskripsi Tingkat log default
CoreEventId.QueryIterationFailed Terjadi kesalahan saat memproses hasil kueri. LogLevel.Error
CoreEventId.SaveChangesFailed Terjadi kesalahan saat mencoba menyimpan perubahan ke database. LogLevel.Error
RelationalEventId.CommandError Terjadi kesalahan saat perintah database sedang dijalankan. LogLevel.Error
CoreEventId.QueryCanceled Kueri dibatalkan. LogLevel.Debug
CoreEventId.SaveChangesCanceled Perintah database dibatalkan saat mencoba menyimpan perubahan. LogLevel.Debug
RelationalEventId.CommandCanceled Eksekusi DbCommand telah dibatalkan. LogLevel.Debug

Catatan

Pembatalan terdeteksi dengan melihat pengecualian daripada memeriksa token pembatalan. Ini berarti bahwa pembatalan yang tidak dipicu melalui token pembatalan masih akan terdeteksi dan dicatat dengan cara ini.

Baru IProperty dan INavigation kelebihan beban untuk EntityEntry metode

Kode yang bekerja dengan model EF akan sering memiliki IPropertyINavigation atau mewakili metadata properti atau navigasi. EntitasEntry kemudian digunakan untuk mendapatkan nilai properti/navigasi atau mengkueri statusnya. Namun, sebelum EF7, ini diperlukan meneruskan nama properti atau navigasi ke metode EntityEntry, yang kemudian akan mencari IProperty kembali atau INavigation. Di EF7, IProperty atau INavigation dapat diteruskan secara langsung, menghindari pencarian tambahan.

Misalnya, pertimbangkan metode untuk menemukan semua saudara kandung dari entitas tertentu:

public static IEnumerable<TEntity> FindSiblings<TEntity>(
    this DbContext context, TEntity entity, string navigationToParent)
    where TEntity : class
{
    var parentEntry = context.Entry(entity).Reference(navigationToParent);

    return context.Entry(parentEntry.CurrentValue!)
        .Collection(parentEntry.Metadata.Inverse!)
        .CurrentValue!
        .OfType<TEntity>()
        .Where(e => !ReferenceEquals(e, entity));
}

Metode ini menemukan induk entitas tertentu, lalu meneruskan inversi INavigation ke Collection metode entri induk. Metadata ini kemudian digunakan untuk mengembalikan semua saudara kandung dari induk yang diberikan. Berikut adalah contoh penggunaannya:


Console.WriteLine($"Siblings to {post.Id}: '{post.Title}' are...");
foreach (var sibling in context.FindSiblings(post, nameof(post.Blog)))
{
    Console.WriteLine($"    {sibling.Id}: '{sibling.Title}'");
}

Dan outputnya:

Siblings to 1: 'Announcing Entity Framework 7 Preview 7: Interceptors!' are...
    5: 'Productivity comes to .NET MAUI in Visual Studio 2022'
    6: 'Announcing .NET 7 Preview 7'
    7: 'ASP.NET Core updates in .NET 7 Preview 7'

EntityEntry untuk jenis entitas jenis bersama

EF Core dapat menggunakan jenis CLR yang sama untuk beberapa jenis entitas yang berbeda. Ini dikenal sebagai "jenis entitas jenis bersama", dan sering digunakan untuk memetakan jenis kamus dengan pasangan kunci/nilai yang digunakan untuk properti jenis entitas. Misalnya, BuildMetadata jenis entitas dapat didefinisikan tanpa menentukan jenis CLR khusus:

modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
    "BuildMetadata", b =>
    {
        b.IndexerProperty<int>("Id");
        b.IndexerProperty<string>("Tag");
        b.IndexerProperty<Version>("Version");
        b.IndexerProperty<string>("Hash");
        b.IndexerProperty<bool>("Prerelease");
    });

Perhatikan bahwa jenis entitas jenis bersama harus diberi nama - dalam hal ini, namanya adalah BuildMetadata. Jenis entitas ini kemudian diakses menggunakan DbSet untuk jenis entitas yang diperoleh menggunakan nama. Contohnya:

public DbSet<Dictionary<string, object>> BuildMetadata
    => Set<Dictionary<string, object>>("BuildMetadata");

Ini DbSet dapat digunakan untuk melacak instans entitas:

await context.BuildMetadata.AddAsync(
    new Dictionary<string, object>
    {
        { "Tag", "v7.0.0-rc.1.22426.7" },
        { "Version", new Version(7, 0, 0) },
        { "Prerelease", true },
        { "Hash", "dc0f3e8ef10eb1464b27f0fd4704f53c01226036" }
    });

Dan jalankan kueri:

var builds = await context.BuildMetadata
    .Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
    .OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
    .ToListAsync();

Sekarang, di EF7, ada juga Entry metode yang DbSet dapat digunakan untuk mendapatkan status instans, bahkan jika belum dilacak. Contohnya:

var state = context.BuildMetadata.Entry(build).State;

ContextInitialized sekarang dicatat sebagai Debug

Di EF7, ContextInitialized peristiwa dicatat di tingkat .Debug Contohnya:

dbug: 10/7/2022 12:27:52.379 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

Dalam rilis sebelumnya, rilis dicatat pada tingkat .Information Contohnya:

info: 10/7/2022 12:30:34.757 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

Jika diinginkan, tingkat log dapat diubah kembali ke Information:

optionsBuilder.ConfigureWarnings(
    builder =>
    {
        builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
    });

IEntityEntryGraphIterator dapat digunakan secara publik

Di EF7, IEntityEntryGraphIterator layanan dapat digunakan oleh aplikasi. Ini adalah layanan yang digunakan secara internal saat menemukan grafik entitas untuk dilacak, dan juga oleh TrackGraph. Berikut adalah contoh yang berulang atas semua entitas yang dapat dijangkau dari beberapa entitas awal:

var blogEntry = context.ChangeTracker.Entries<Blog>().First();
var found = new HashSet<object>();
var iterator = context.GetService<IEntityEntryGraphIterator>();
iterator.TraverseGraph(new EntityEntryGraphNode<HashSet<object>>(blogEntry, found, null, null), node =>
{
    if (node.NodeState.Contains(node.Entry.Entity))
    {
        return false;
    }

    Console.Write($"Found with '{node.Entry.Entity.GetType().Name}'");

    if (node.InboundNavigation != null)
    {
        Console.Write($" by traversing '{node.InboundNavigation.Name}' from '{node.SourceEntry!.Entity.GetType().Name}'");
    }

    Console.WriteLine();

    node.NodeState.Add(node.Entry.Entity);

    return true;
});

Console.WriteLine();
Console.WriteLine($"Finished iterating. Found {found.Count} entities.");
Console.WriteLine();

Pemberitahuan:

  • Iterator berhenti melintas dari simpul tertentu ketika delegasi panggilan balik mengembalikan false. Contoh ini melacak entitas yang dikunjungi dan kembali false ketika entitas telah dikunjungi. Ini mencegah perulangan tak terbatas yang dihasilkan dari siklus dalam grafik.
  • Objek EntityEntryGraphNode<TState> memungkinkan status untuk diteruskan tanpa menangkapnya ke delegasi.
  • Untuk setiap simpul yang dikunjungi selain yang pertama, simpul itu ditemukan dari dan navigasi yang ditemukan melalui diteruskan ke panggilan balik.

Peningkatan bangunan model

EF7 berisi berbagai peningkatan kecil dalam pembuatan model.

Tip

Kode untuk sampel di bagian ini berasal dari ModelBuildingSample.cs.

Indeks bisa naik atau menurun

Secara default, EF Core membuat indeks naik. EF7 juga mendukung pembuatan indeks menurun. Contohnya:

modelBuilder
    .Entity<Post>()
    .HasIndex(post => post.Title)
    .IsDescending();

Atau, menggunakan Index atribut pemetaan:

[Index(nameof(Title), AllDescending = true)]
public class Post
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Title { get; set; }
}

Ini jarang berguna untuk indeks melalui satu kolom, karena database dapat menggunakan indeks yang sama untuk mengurutkan di kedua arah. Namun, ini tidak terjadi untuk indeks komposit melalui beberapa kolom di mana urutan pada setiap kolom bisa menjadi penting. EF Core mendukung ini dengan memungkinkan beberapa kolom memiliki urutan yang berbeda yang ditentukan untuk setiap kolom. Contohnya:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner })
    .IsDescending(false, true);

Atau, menggunakan atribut pemetaan:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Ini menghasilkan SQL berikut saat menggunakan SQL Server:

CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);

Terakhir, beberapa indeks dapat dibuat melalui kumpulan kolom yang diurutkan yang sama dengan memberikan nama indeks. Contohnya:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_1")
    .IsDescending(false, true);

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_2")
    .IsDescending(true, true);

Atau, menggunakan atribut pemetaan:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true }, Name = "IX_Blogs_Name_Owner_1")]
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { true, true }, Name = "IX_Blogs_Name_Owner_2")]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Ini menghasilkan SQL berikut di SQL Server:

CREATE INDEX [IX_Blogs_Name_Owner_1] ON [Blogs] ([Name], [Owner] DESC);
CREATE INDEX [IX_Blogs_Name_Owner_2] ON [Blogs] ([Name] DESC, [Owner] DESC);

Atribut pemetaan untuk kunci komposit

EF7 memperkenalkan atribut pemetaan baru (alias "anotasi data") untuk menentukan properti kunci utama atau properti dari jenis entitas apa pun. Tidak seperti System.ComponentModel.DataAnnotations.KeyAttribute, PrimaryKeyAttribute ditempatkan pada kelas jenis entitas daripada pada properti kunci. Contohnya:

[PrimaryKey(nameof(PostKey))]
public class Post
{
    public int PostKey { get; set; }
}

Ini membuatnya sangat cocok untuk menentukan kunci komposit:

[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
    public int PostId { get; set; }
    public int CommentId { get; set; }
    public string CommentText { get; set; } = null!;
}

Menentukan indeks pada kelas juga berarti dapat digunakan untuk menentukan properti atau bidang privat sebagai kunci, meskipun ini biasanya akan diabaikan saat membangun model EF. Contohnya:

[PrimaryKey(nameof(_id))]
public class Tag
{
    private readonly int _id;
}

DeleteBehavior atribut pemetaan

EF7 memperkenalkan atribut pemetaan (alias "anotasi data") untuk menentukan DeleteBehavior untuk hubungan. Misalnya, hubungan yang diperlukan dibuat dengan secara DeleteBehavior.Cascade default. Ini dapat diubah menjadi DeleteBehavior.NoAction secara default menggunakan DeleteBehaviorAttribute:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [DeleteBehavior(DeleteBehavior.NoAction)]
    public Blog Blog { get; set; } = null!;
}

Ini akan menonaktifkan penghapusan bertingkat untuk hubungan Blog-Posts.

Properti dipetakan ke nama kolom yang berbeda

Beberapa pola pemetaan mengakibatkan properti CLR yang sama dipetakan ke kolom di masing-masing dari beberapa tabel yang berbeda. EF7 memungkinkan kolom ini memiliki nama yang berbeda. Misalnya, pertimbangkan hierarki pewarisan sederhana:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

Dengan strategi pemetaan warisan TPT, jenis ini akan dipetakan ke tiga tabel. Namun, kolom kunci utama di setiap tabel mungkin memiliki nama yang berbeda. Contohnya:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 memungkinkan pemetaan ini dikonfigurasi menggunakan penyusun tabel berlapis:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

Dengan pemetaan pewarisan TPC, Breed properti juga dapat dipetakan ke nama kolom yang berbeda dalam tabel yang berbeda. Misalnya, pertimbangkan tabel TPC berikut:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 mendukung pemetaan tabel ini:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });

Hubungan banyak-ke-banyak unidirectional

EF7 mendukung hubungan banyak ke banyak di mana satu sisi atau sisi lainnya tidak memiliki properti navigasi. Misalnya, pertimbangkan Post dan Tag jenis:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
}
public class Tag
{
    public int Id { get; set; }
    public string TagName { get; set; } = null!;
}

Perhatikan bahwa jenis memiliki Post properti navigasi untuk daftar tag, tetapi jenisnya Tag tidak memiliki properti navigasi untuk postingan. Dalam EF7, ini masih dapat dikonfigurasi sebagai hubungan banyak-ke-banyak, memungkinkan objek yang sama Tag digunakan untuk banyak posting yang berbeda. Contohnya:

modelBuilder
    .Entity<Post>()
    .HasMany(post => post.Tags)
    .WithMany();

Ini menghasilkan pemetaan ke tabel gabungan yang sesuai:

CREATE TABLE [Tags] (
    [Id] int NOT NULL IDENTITY,
    [TagName] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Tags] PRIMARY KEY ([Id])
);

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(64) NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
);

CREATE TABLE [PostTag] (
    [PostId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE
);

Dan hubungan dapat digunakan sebagai banyak-ke-banyak dengan cara normal. Misalnya, menyisipkan beberapa postingan yang berbagi berbagai tag dari set umum:

var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, };

await context.AddRangeAsync(new Blog { Posts =
{
    new Post { Tags = { tags[0], tags[1] } },
    new Post { Tags = { tags[1], tags[0], tags[2] } },
    new Post()
} });

await context.SaveChangesAsync();

Pemisahan entitas

Pemisahan entitas memetakan satu jenis entitas ke beberapa tabel. Misalnya, pertimbangkan database dengan tiga tabel yang menyimpan data pelanggan:

  • Tabel Customers untuk informasi pelanggan
  • Tabel PhoneNumbers untuk nomor telepon pelanggan
  • Tabel Addresses untuk alamat pelanggan

Berikut adalah definisi untuk tabel ini di SQL Server:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

Masing-masing tabel ini biasanya akan dipetakan ke jenis entitas mereka sendiri, dengan hubungan antara jenis. Namun, jika ketiga tabel selalu digunakan bersama-sama, maka akan lebih nyaman untuk memetakan semuanya ke satu jenis entitas. Contohnya:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Ini dicapai di EF7 dengan memanggil SplitToTable untuk setiap pemisahan dalam jenis entitas. Misalnya, kode berikut membagi Customer jenis entitas ke Customerstabel , , PhoneNumbersdan Addresses yang ditunjukkan di atas:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

Perhatikan juga bahwa, jika perlu, nama kolom kunci utama yang berbeda dapat ditentukan untuk setiap tabel.

String SQL Server UTF-8

String Unicode SQL Server seperti yang diwakili oleh nchar jenis data dan nvarchar disimpan sebagai UTF-16. Selain itu, char jenis data dan varchar digunakan untuk menyimpan string non-Unicode dengan dukungan untuk berbagai set karakter.

Dimulai dengan SQL Server 2019, char jenis data dan varchar dapat digunakan untuk menyimpan string Unicode dengan pengodean UTF-8 . dicapai dengan mengatur salah satu kolase UTF-8. Misalnya, kode berikut mengonfigurasi string SQL Server UTF-8 panjang variabel untuk CommentText kolom:

modelBuilder
    .Entity<Comment>()
    .Property(comment => comment.CommentText)
    .HasColumnType("varchar(max)")
    .UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");

Konfigurasi ini menghasilkan definisi kolom SQL Server berikut:

CREATE TABLE [Comment] (
    [PostId] int NOT NULL,
    [CommentId] int NOT NULL,
    [CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL,
    CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId])
);

Tabel temporal mendukung entitas yang dimiliki

Pemetaan tabel temporal EF Core SQL Server telah ditingkatkan di EF7 untuk mendukung berbagi tabel. Terutama, pemetaan default untuk entitas tunggal yang dimiliki menggunakan berbagi tabel.

Misalnya, pertimbangkan jenis Employee entitas pemilik dan jenis EmployeeInfoentitas yang dimilikinya :

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; } = null!;

    public EmployeeInfo Info { get; set; } = null!;
}

public class EmployeeInfo
{
    public string Position { get; set; } = null!;
    public string Department { get; set; } = null!;
    public string? Address { get; set; }
    public decimal? AnnualSalary { get; set; }
}

Jika jenis ini dipetakan ke tabel yang sama, maka di EF7 tabel tersebut dapat dibuat tabel temporal:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        tableBuilder =>
        {
            tableBuilder.IsTemporal();
            tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
            tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
        })
    .OwnsOne(
        employee => employee.Info,
        ownedBuilder => ownedBuilder.ToTable(
            "Employees",
            tableBuilder =>
            {
                tableBuilder.IsTemporal();
                tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
                tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
            }));

Catatan

Membuat konfigurasi ini lebih mudah dilacak oleh Masalah #29303. Pilih masalah ini jika itu adalah sesuatu yang ingin Anda lihat diimplementasikan.

Pembuatan nilai yang disempurnakan

EF7 mencakup dua peningkatan signifikan pada pembuatan nilai otomatis untuk properti utama.

Tip

Kode untuk sampel di bagian ini berasal dari ValueGenerationSample.cs.

Pembuatan nilai untuk jenis yang dijaga DDD

Dalam desain berbasis domain (DDD), "kunci yang dijaga" dapat meningkatkan keamanan jenis properti kunci. Ini dicapai dengan membungkus jenis kunci dalam jenis lain yang khusus untuk penggunaan kunci. Misalnya, kode berikut menentukan ProductId jenis untuk kunci produk, dan CategoryId jenis untuk kunci kategori.

public readonly struct ProductId
{
    public ProductId(int value) => Value = value;
    public int Value { get; }
}

public readonly struct CategoryId
{
    public CategoryId(int value) => Value = value;
    public int Value { get; }
}

Ini kemudian digunakan dalam Product dan Category jenis entitas:

public class Product
{
    public Product(string name) => Name = name;
    public ProductId Id { get; set; }
    public string Name { get; set; }
    public CategoryId CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class Category
{
    public Category(string name) => Name = name;
    public CategoryId Id { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; } = new();
}

Ini membuatnya tidak mungkin untuk secara tidak sengaja menetapkan ID untuk kategori ke produk, atau sebaliknya.

Peringatan

Seperti banyak konsep DDD, keamanan jenis yang ditingkatkan ini mengorbankan kompleksitas kode tambahan. Perlu dipertimbangkan apakah, misalnya, menetapkan ID produk ke kategori adalah sesuatu yang kemungkinan akan terjadi. Menjaga hal-hal sederhana mungkin secara keseluruhan lebih bermanfaat bagi basis kode.

Jenis kunci yang dijaga yang ditampilkan di sini keduanya membungkus int nilai kunci, yang berarti nilai bilangan bulat akan digunakan dalam tabel database yang dipetakan. Ini dicapai dengan menentukan pengonversi nilai untuk jenis:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
    configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}

private class ProductIdConverter : ValueConverter<ProductId, int>
{
    public ProductIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

private class CategoryIdConverter : ValueConverter<CategoryId, int>
{
    public CategoryIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

Catatan

Kode di sini menggunakan struct jenis. Ini berarti mereka memiliki semantik jenis nilai yang sesuai untuk digunakan sebagai kunci. Jika class jenis digunakan sebagai gantinya, maka mereka perlu mengambil alih semantik kesetaraan atau juga menentukan pembanding nilai.

Di EF7, jenis kunci berdasarkan pengonversi nilai dapat menggunakan nilai kunci yang dihasilkan secara otomatis selama jenis yang mendasar mendukung ini. Ini dikonfigurasi dengan cara normal menggunakan ValueGeneratedOnAdd:

modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();

Secara default, ini menghasilkan IDENTITY kolom saat digunakan dengan SQL Server:

CREATE TABLE [Categories] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Categories] PRIMARY KEY ([Id]));

CREATE TABLE [Products] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Yang digunakan dengan cara normal untuk menghasilkan nilai kunci saat menyisipkan entitas:

MERGE [Categories] USING (
VALUES (@p0, 0),
(@p1, 1)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;

Pembuatan kunci berbasis urutan untuk SQL Server

EF Core mendukung pembuatan nilai kunci menggunakan kolom SQL ServerIDENTITY, atau pola Hi-Lo berdasarkan blok kunci yang dihasilkan oleh urutan database. EF7 memperkenalkan dukungan untuk urutan database yang dilampirkan ke batasan default kolom kunci. Dalam bentuk yang paling sederhana, ini hanya mengharuskan memberi tahu EF Core untuk menggunakan urutan untuk properti kunci:

modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();

Ini menghasilkan urutan yang ditentukan dalam database:

CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;

Yang kemudian digunakan dalam batasan default kolom kunci:

CREATE TABLE [Products] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]),
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Catatan

Bentuk pembuatan kunci ini digunakan secara default untuk kunci yang dihasilkan dalam hierarki jenis entitas menggunakan strategi pemetaan TPC.

Jika diinginkan, urutannya dapat diberi nama dan skema yang berbeda. Contohnya:

modelBuilder
    .Entity<Product>()
    .Property(product => product.Id)
    .UseSequence("ProductsSequence", "northwind");

Konfigurasi urutan lebih lanjut dibentuk dengan mengonfigurasinya secara eksplisit dalam model. Contohnya:

modelBuilder
    .HasSequence<int>("ProductsSequence", "northwind")
    .StartsAt(1000)
    .IncrementsBy(2);

Peningkatan alat migrasi

EF7 mencakup dua peningkatan signifikan saat menggunakan alat baris perintah EF Core Migrations.

UseSqlServer dll. terima null

Sangat umum untuk membaca string koneksi dari file konfigurasi dan kemudian meneruskan string koneksi tersebut ke UseSqlServer, UseSqlite, atau metode yang setara untuk penyedia lain. Contohnya:

services.AddDbContext<BloggingContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));

Juga umum untuk melewati string koneksi saat menerapkan migrasi. Contohnya:

dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Atau saat menggunakan bundel Migrasi.

./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Dalam hal ini, meskipun string koneksi membaca dari konfigurasi tidak digunakan, kode startup aplikasi masih mencoba membacanya dari konfigurasi dan meneruskannya ke UseSqlServer. Jika konfigurasi tidak tersedia, maka ini menghasilkan meneruskan null ke UseSqlServer. Di EF7, ini diizinkan, selama string koneksi pada akhirnya diatur nanti, seperti dengan meneruskan --connection ke alat baris perintah.

Catatan

Perubahan ini telah dilakukan untuk UseSqlServer dan UseSqlite. Untuk penyedia lain, hubungi pengelola penyedia untuk membuat perubahan yang setara jika belum dilakukan untuk penyedia tersebut.

Mendeteksi kapan alat berjalan

EF Core menjalankan kode aplikasi saat dotnet-ef perintah atau PowerShell sedang digunakan. Terkadang mungkin perlu untuk mendeteksi situasi ini untuk mencegah kode yang tidak pantas dijalankan pada waktu desain. Misalnya, kode yang secara otomatis menerapkan migrasi saat startup mungkin tidak boleh melakukan ini pada waktu desain. Di EF7, ini dapat dideteksi menggunakan EF.IsDesignTime bendera:

if (!EF.IsDesignTime)
{
    await context.Database.MigrateAsync();
}

EF Core mengatur ke IsDesignTimetrue ketika kode aplikasi berjalan atas nama alat.

Peningkatan performa untuk proksi

EF Core mendukung proksi yang dihasilkan secara dinamis untuk pemuatan malas dan pelacakan perubahan. EF7 berisi dua peningkatan performa saat menggunakan proksi ini:

  • Jenis proksi sekarang dibuat dengan malas. Ini berarti bahwa waktu pembuatan model awal saat menggunakan proksi dapat jauh lebih cepat dengan EF7 daripada dengan EF Core 6.0.
  • Proksi sekarang dapat digunakan dengan model yang dikompilasi.

Berikut adalah beberapa hasil performa untuk model dengan 449 jenis entitas, 6390 properti, dan 720 hubungan.

Skenario Metode Rata-rata Kesalahan StdDev
EF Core 6.0 tanpa proksi TimeToFirstQuery 1,085 dtk 0,0083 dtk 0,0167 dtk
EF Core 6.0 dengan proksi pelacakan perubahan TimeToFirstQuery 13,01 dtk 0.2040 dtk 0,4110 dtk
EF Core 7.0 tanpa proksi TimeToFirstQuery 1,442 dtk 0,0134 dtk 0,0272 dtk
EF Core 7.0 dengan proksi pelacakan perubahan TimeToFirstQuery 1,446 dtk 0,0160 dtk 0,0323 dtk
EF Core 7.0 dengan proksi pelacakan perubahan dan model yang dikompilasi TimeToFirstQuery 0,162 dtk 0,0062 dtk 0,0125 dtk

Jadi, dalam hal ini, model dengan proksi pelacakan perubahan dapat siap untuk menjalankan kueri pertama 80 kali lebih cepat di EF7 daripada yang dimungkinkan dengan EF Core 6.0.

Pengikatan data Formulir Windows kelas satu

Tim Formulir Windows telah melakukan beberapa peningkatan besar pada pengalaman Visual Studio Designer. Ini termasuk pengalaman baru untuk pengikatan data yang terintegrasi dengan baik dengan EF Core.

Singkatnya, pengalaman baru ini menyediakan Visual Studio U.I. untuk membuat ObjectDataSource:

Choose Category data source type

Ini kemudian dapat terikat ke EF Core DbSet dengan beberapa kode sederhana:

public partial class MainForm : Form
{
    private ProductsContext? dbContext;

    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.dbContext = new ProductsContext();

        this.dbContext.Categories.Load();
        this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);

        this.dbContext?.Dispose();
        this.dbContext = null;
    }
}

Lihat Memulai Formulir Windows untuk panduan lengkap dan aplikasi sampel WinForms yang dapat diunduh.