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_Phone |
---|---|---|---|---|---|---|
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]
ExecuteDelete
Seperti 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, lihathttps://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 atauPet
, karena ini adaabstract
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 memilikiVet
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:
Kueri yang mengembalikan entitas dari semua jenis dalam hierarki:
context.Animals.ToList();
Kueri yang mengembalikan entitas dari subset jenis dalam hierarki:
context.Pets.ToList();
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:
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]
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')
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:
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]
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]
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.
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]
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]
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 (e.g. SQL Server), maka 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 FavoriteAnimalId
kunci 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 diOnModelCreating
DataAnnotation
: Elemen model dikonfigurasi menggunakan atribut pemetaan (alias anotasi data) pada jenis CLRConvention
: 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 MaxStringLengthConvention
DiscriminatorLengthConvention3
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 [Persist]
(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
, , UpdateUsingStoredProcedure
dan 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 "People", 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 untukRowVersion
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 jikaRowVersion
nilai asli cocok. - Nilai baru yang dihasilkan untuk
RowVersion
disisipkan ke dalam tabel sementara. - Jumlah baris yang terpengaruh (
@@ROWCOUNT
) dan nilai yang dihasilkanRowVersion
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 peristiwa dan pengelogan .NET tradisional.
EF7 mencakup penyempurnaan berikut untuk pencegat:
- Intersepsi untuk membuat dan mengisi instans entitas baru (alias "materialisasi")
- Intersepsi untuk mengubah pohon ekspresi LINQ sebelum kueri dikompilasi
- Intersepsi untuk penanganan konkurensi optimis (
DbUpdateConcurrencyException
) - Intersepsi untuk koneksi sebelum memeriksa apakah string koneksi telah diatur
- Intersepsi ketika EF Core telah selesai mengonsumsi tataan hasil, tetapi sebelum tataan hasil tersebut ditutup
- Intersepsi untuk pembuatan
DbConnection
oleh EF Core - Intersepsi untuk
DbCommand
setelah diinisialisasi
Selain itu, EF7 menyertakan peristiwa .NET tradisional baru untuk:
- Ketika entitas akan dilacak atau mengubah status, tetapi sebelum benar-benar dilacak atau mengubah status
- Sebelum dan sesudah EF Core mendeteksi perubahan pada entitas dan properti (alias
DetectChanges
intersepsi)
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 ditampilkan 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 keId
propertiIHasIntKey
antarmuka.Input ke dalam panggilan ini adalah asli
OrderBy(e => ...)
, sehingga hasil akhirnya adalah ekspresi untukOrderBy(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
SaveChanges
SaveChangesAsync
. Namun, jika semua kode aplikasi asinkron, maka hanyaThrowingConcurrencyExceptionAsync
perlu diimplementasikan. Demikian juga, jika aplikasi tidak pernah menggunakan metode database asinkron, maka hanyaThrowingConcurrencyException
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, melemparkanDbUpdateConcurrencyException
. 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 LazyConnectionStringSample.cs.
String koneksi 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. ConnectionOpeningAsync
adalah 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 ditampilkan 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:
- Terjemahan dan
String.Join
String.Concat
- Terjemahan fungsi agregat spasial
- Terjemahan fungsi agregat statistik
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:
- GeometryCombiner.Combine()
- UnaryUnionOp.Union()
- ConvexHull.Create()
- EnvelopeCombiner.CombineAsGeometry()
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 DbContext
DbContext
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:
Kejadian | 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 IProperty INavigation 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 kembalifalse
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 Customers
tabel , , PhoneNumbers
dan 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 EmployeeInfo
entitas 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 IsDesignTime
true
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:
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.