EF Core 7.0'daki Yenilikler
EF Core 7.0 (EF7), Kasım 2022'de piyasaya sürüldü.
İpucu
GitHub'dan örnek kodu indirerek örnekleri çalıştırabilir ve hata ayıklayabilirsiniz. Her bölüm, bu bölüme özgü kaynak koduna bağlanır.
EF7 , .NET 6'yı hedefler ve bu nedenle .NET 6 (LTS) veya .NET 7 ile kullanılabilir.
Örnek model
Aşağıdaki örneklerin birçoğu bloglar, gönderiler, etiketler ve yazarlarla basit bir model kullanır:
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();
}
Örneklerden bazıları, farklı örneklerde farklı şekillerde eşlenen toplama türlerini de kullanır. Kişiler için bir toplama türü vardır:
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; }
}
Gönderi meta verileri için ikinci bir toplama türü:
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; }
}
İpucu
Örnek model BlogsContext.cs bulunabilir.
JSON Sütunları
İlişkisel veritabanlarının çoğu JSON belgeleri içeren sütunları destekler. Bu sütunlardaki JSON dosyasında sorgularla detaya gidebilirsiniz. Bu, örneğin, belgelerin öğelerine göre filtreleme ve sıralamanın yanı sıra belgelerin dışından öğelerin sonuçlara yansıtılmasını sağlar. JSON sütunları ilişkisel veritabanlarının belge veritabanlarının bazı özelliklerini almasına olanak tanıyarak ikisi arasında kullanışlı bir karma oluşturur.
EF7, SQL Server uygulamasıyla birlikte JSON sütunları için sağlayıcıdan bağımsız destek içerir. Bu destek, .NET türlerinden oluşturulan toplamaların JSON belgelerine eşlenmesini sağlar. Normal LINQ sorguları toplamalarda kullanılabilir ve bunlar JSON'da detaya gitmek için gereken uygun sorgu yapılarına çevrilir. EF7 ayrıca JSON belgelerinin güncelleştirilmesini ve kaydedilmesini de destekler.
Not
EF7 sonrası için JSON için SQLite desteği planlanıyor. PostgreSQL ve Pomelo MySQL sağlayıcıları JSON sütunları için zaten bazı destek içerir. JSON desteğini tüm sağlayıcılarda uyumlu hale getirmek için bu sağlayıcıların yazarlarıyla birlikte çalışacağız.
JSON sütunlarına eşleme
EF Core'da toplama türleri ve OwnsMany
kullanılarak OwnsOne
tanımlanır. Örneğin, kişi bilgilerini depolamak için kullanılan örnek modelimizin toplama türünü göz önünde bulundurun:
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; }
}
Bu daha sonra bir yazarın kişi ayrıntılarını depolamak için "sahip" varlık türünde kullanılabilir:
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ContactDetails Contact { get; set; }
}
Toplama türü kullanılarak OwnsOne
yapılandırılırOnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
İpucu
Burada gösterilen kod JsonColumnsSample.cs gelir.
varsayılan olarak, ilişkisel veritabanı sağlayıcıları bunun gibi toplama türlerini sahip olan varlık türüyle aynı tabloyla eşler. Diğer bir ifadeyle, ve Address
sınıflarının ContactDetails
her özelliği tablodaki bir sütuna Authors
eşlenir.
Kişi ayrıntıları olan bazı kayıtlı yazarlar şöyle görünür:
Yazar
Id | Veri Akışı Adı | Contact_Address_Street | Contact_Address_City | Contact_Address_Postcode | Contact_Address_Country | Contact_Phone |
---|---|---|---|---|---|---|
1 | Maddy Montaquila | 1 Ana Cadde | Camberwick Yeşili | CW1 5ZH | Birleşik Krallık | 01632 12345 |
2 | Jeremy Likness | 2 Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık | 01632 12346 |
3 | Daniel Roth | 3 Ana Cadde | Camberwick Yeşili | CW1 5ZH | Birleşik Krallık | 01632 12347 |
4 | Arthur Vickers | 15a Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık | 01632 22345 |
5 | Brice Lambson | 4 Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık | 01632 12349 |
İsterseniz, toplamayı oluşturan her varlık türü kendi tablosuna eşlenebilir:
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");
});
});
}
Daha sonra aynı veriler üç tabloda depolanır:
Yazar
Id | Veri Akışı Adı |
---|---|
1 | Maddy Montaquila |
2 | Jeremy Likness |
3 | Daniel Roth |
4 | Arthur Vickers |
5 | Brice Lambson |
İlgili Kişiler
AuthorId | Telefon |
---|---|
1 | 01632 12345 |
2 | 01632 12346 |
3 | 01632 12347 |
4 | 01632 22345 |
5 | 01632 12349 |
Adresler
ContactDetailsAuthorId | Sokak | City | Posta kodu | Ülke |
---|---|---|---|---|
1 | 1 Ana Cadde | Camberwick Yeşili | CW1 5ZH | Birleşik Krallık |
2 | 2 Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık |
3 | 3 Ana Cadde | Camberwick Yeşili | CW1 5ZH | Birleşik Krallık |
4 | 15a Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık |
5 | 4 Ana Cadde | Kıkırdak | CW1 5ZH | Birleşik Krallık |
Şimdi, ilginç kısmı için. EF7'de toplama ContactDetails
türü bir JSON sütununa eşlenebilir. Bu, toplama türünü yapılandırırken yalnızca bir çağrı ToJson()
gerektirir:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToJson();
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Tabloda Authors
artık her yazar için ContactDetails
bir JSON belgesiyle doldurulan bir JSON sütunu yer alır:
Yazar
Id | Veri Akışı Adı | İlgili kişi |
---|---|---|
1 | Maddy Montaquila | { "Phone":"01632 12345", "Adres": { "City":"Camberwick Green", "Ülke":"UK", "Postcode":"CW1 5ZH", "Street":"1 Main St" } } |
2 | Jeremy Likness | { "Telefon":"01632 12346", "Adres": { "Şehir":"Chigley", "Ülke":"UK", "Postcode":"CH1 5ZH", "Street":"2 Main St" } } |
3 | Daniel Roth | { "Telefon":"01632 12347", "Adres": { "City":"Camberwick Green", "Ülke":"UK", "Postcode":"CW1 5ZH", "Street":"3 Main St" } } |
4 | Arthur Vickers | { "Telefon":"01632 12348", "Adres": { "Şehir":"Chigley", "Ülke":"UK", "Postcode":"CH1 5ZH", "Street":"15a Main St" } } |
5 | Brice Lambson | { "Telefon":"01632 12349", "Adres": { "Şehir":"Chigley", "Ülke":"UK", "Postcode":"CH1 5ZH", "Street":"4 Main St" } } |
İpucu
Toplamaların bu kullanımı, Azure Cosmos DB için EF Core sağlayıcısı kullanılırken JSON belgelerinin eşlenme yöntemine çok benzer. JSON sütunları, ef core kullanarak belge veritabanlarını ilişkisel bir veritabanına katıştırılmış belgelere getirme olanağı sunar.
Yukarıda gösterilen JSON belgeleri çok basittir, ancak bu eşleme özelliği daha karmaşık belge yapılarıyla da kullanılabilir. Örneğin, örnek modelimizin bir gönderi hakkındaki meta verileri temsil etmek için kullanılan başka bir toplama türünü göz önünde bulundurun:
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; }
}
Bu toplama türü birkaç iç içe tür ve koleksiyon içerir. OwnsOne
ve OwnsMany
çağrıları bu toplama türünü eşlemek için kullanılır:
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));
});
İpucu
ToJson
yalnızca toplamanın tamamını bir JSON belgesine eşlemek için toplama kökünde gereklidir.
Bu eşlemeyle EF7, aşağıdaki gibi karmaşık bir JSON belgesi oluşturabilir ve sorgulayabilir:
{
"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"
}
]
}
]
}
Not
Uzamsal türleri doğrudan JSON'a eşleme henüz desteklenmiyor. Yukarıdaki belgede geçici çözüm olarak değerler kullanılır double
. İlgilendiğiniz bir konuysa JSON sütunlarında Uzamsal türleri destekleme için oy verin.
Not
İlkel tür koleksiyonlarının JSON ile eşlenmesi henüz desteklenmiyor. Yukarıdaki belge, koleksiyonu virgülle ayrılmış bir dizeye dönüştürmek için bir değer dönüştürücüsü kullanır. Json için oy verin: bu ilgilendiğiniz bir şeyse ilkel türlerin toplanması için destek ekleyin.
Not
Sahip olunan türlerin JSON'a eşlenmesi henüz TPT veya TPC devralma ile birlikte desteklenmiyor. İlgilendiğiniz bir şeyse TPT/TPC devralma eşlemesi ile JSON özelliklerini destekleme için oy verin.
JSON sütunlarına sorgular
JSON sütunlarına yapılan sorgular, EF Core'daki diğer toplama türlerinde sorgulamayla aynı şekilde çalışır. Yani, LINQ kullanın! İşte bazı örnekler.
Chigley'de yaşayan tüm yazarlar için bir sorgu:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'i oluşturur:
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'
JSON belgesinin içinden almak City
Address
için kullanımına JSON_VALUE
dikkat edin.
Select
JSON belgesinden öğeleri ayıklamak ve yansıtmak için kullanılabilir:
var postcodesInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.Select(author => author.Contact.Address.Postcode)
.ToListAsync();
Bu sorgu aşağıdaki SQL'i oluşturur:
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'
Filtre ve projeksiyonda biraz daha fazlasının yanı sıra JSON belgesindeki telefon numarasına göre sipariş veren bir örnek aşağıda verilmişti:
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();
Bu sorgu aşağıdaki SQL'i oluşturur:
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))
JSON belgesi koleksiyonlar içerdiğinde, bunlar sonuçlarda yansıtılabilir:
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();
Bu sorgu aşağıdaki SQL'i oluşturur:
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
Not
JSON koleksiyonları içeren daha karmaşık sorgular için destek gerekir jsonpath
. İlgilendiğiniz bir şey varsa Destek jsonpath sorgulaması için oy verin.
İpucu
JSON belgelerinde sorgu performansını geliştirmek için dizinler oluşturmayı göz önünde bulundurun. Örneğin, bkz . SQL Server kullanırken Json verilerini dizine ekleme.
JSON sütunlarını güncelleştirme
SaveChanges
ve SaveChangesAsync
bir JSON sütununda güncelleştirmeler yapmak için normal şekilde çalışın. Kapsamlı değişiklikler için belgenin tamamı güncelleştirilir. Örneğin, bir yazar için belgenin Contact
büyük bölümünü değiştirme:
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();
Bu durumda, yeni belgenin tamamı parametre olarak geçirilir:
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']
Daha sonra SQL'de UPDATE
kullanılır:
UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Ancak, yalnızca bir alt belge değiştirilirse EF Core yalnızca alt belgeyi güncelleştirmek için bir JSON_MODIFY
komut kullanır. Örneğin, belgenin Contact
içini Address
değiştirme:
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
await context.SaveChangesAsync();
Aşağıdaki parametreleri oluşturur:
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']
Bir çağrı aracılığıyla JSON_MODIFY
içinde UPDATE
kullanılan:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;
Son olarak, yalnızca tek bir özellik değiştirilirse EF Core bu kez yalnızca değiştirilen özellik değerine düzeltme eki uygulamak için bir "JSON_MODIFY" komutu kullanır. Örneğin:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
Aşağıdaki parametreleri oluşturur:
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']
Bu da bir JSON_MODIFY
ile yeniden kullanılır:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
ExecuteUpdate ve ExecuteDelete (Toplu güncelleştirmeler)
Varsayılan olarak, EF Core varlıklardaki değişiklikleri izler ve yöntemlerden biri SaveChanges
çağrıldığında veritabanına güncelleştirmeler gönderir. Değişiklikler yalnızca gerçekten değişmiş özellikler ve ilişkiler için gönderilir. Ayrıca, izlenen varlıklar veritabanına gönderilen değişikliklerle eşitlenmiş durumda kalır. Bu mekanizma, veritabanına genel amaçlı eklemeler, güncelleştirmeler ve silmeler göndermenin verimli ve kullanışlı bir yoludur. Bu değişiklikler, veritabanı gidiş dönüş sayısını azaltmak için de toplu olarak oluşturulur.
Ancak bazen değişiklik izleyicisini dahil etmeden veritabanında güncelleştirme veya silme komutları yürütmek yararlı olabilir. EF7, bunu yeni ExecuteUpdate ve ExecuteDelete yöntemleriyle etkinleştirir. Bu yöntemler bir LINQ sorgusuna uygulanır ve veritabanındaki varlıkları bu sorgunun sonuçlarına göre güncelleştirir veya siler. Birçok varlık tek bir komutla güncelleştirilebilir ve varlıklar belleğe yüklenmez, bu da daha verimli güncelleştirmelere ve silmelere neden olabileceği anlamına gelir.
Ancak şunları unutmayın:
- Yapılacak belirli değişiklikler açıkça belirtilmelidir; EF Core tarafından otomatik olarak algılanmaz.
- İzlenen varlıklar eşitlenmez.
- Veritabanı kısıtlamalarını ihlal etmemesi için ek komutların doğru sırada gönderilmesi gerekebilir. Örneğin, bir sorumlu silinemeden önce bağımlıları silme.
Tüm bunlar, ve ExecuteDelete
yöntemlerinin ExecuteUpdate
mevcut SaveChanges
mekanizmayı değiştirmek yerine tamamlayacakları anlamına gelir.
Temel ExecuteDelete
örnekler
İpucu
Burada gösterilen kod ExecuteDeleteSample.cs gelir.
veya ExecuteDelete
ExecuteDeleteAsync
çağrısı, DbSet
veritabanındaki DbSet
tüm varlıklarını hemen siler. Örneğin, tüm Tag
varlıkları silmek için:
await context.Tags.ExecuteDeleteAsync();
Bu, SQL Server kullanılırken aşağıdaki SQL'i yürütür:
DELETE FROM [t]
FROM [Tags] AS [t]
Daha da ilginç olan sorgu bir filtre içerebilir. Örneğin:
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();
Bu işlem aşağıdaki SQL'i yürütür:
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'
Sorgu, diğer türlerdeki gezintiler de dahil olmak üzere daha karmaşık filtreler de kullanabilir. Örneğin, etiketleri yalnızca eski blog gönderilerinden silmek için:
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();
Yürütülür:
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))
Temel ExecuteUpdate
örnekler
İpucu
Burada gösterilen kod ExecuteUpdateSample.cs gelir.
ExecuteUpdate
ve ExecuteUpdateAsync
yöntemlere çok benzer bir şekilde davranın ExecuteDelete
. Temel fark, bir güncelleştirmenin hangi özelliklerin güncelleştirildiğini ve bunların nasıl güncelleştirildiğini bilmeyi gerektirmesidir. Bu, öğesine SetProperty
yapılan bir veya daha fazla çağrı kullanılarak elde edilir. Örneğin, her blogun Name
güncelleştirmesini yapmak için:
await context.Blogs.ExecuteUpdateAsync(
s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));
öğesinin ilk parametresi hangi özelliğin SetProperty
güncelleştirilecek olduğunu belirtir; bu durumda, Blog.Name
. İkinci parametre, yeni değerin nasıl hesaplanması gerektiğini belirtir; bu durumda, mevcut değeri alıp ekleyerek "*Featured!*"
. Sonuçta elde edilen SQL şöyledir:
UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]
gibi ExecuteDelete
sorgu da güncelleştirilen varlıkları filtrelemek için kullanılabilir. Ayrıca, hedef varlıkta birden fazla özelliği güncelleştirmek için birden çok çağrısı SetProperty
kullanılabilir. Örneğin, Title
2022'ye kadar yayımlanan tüm gönderilerin ve Content
güncelleştirmeleri için:
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 + ")"));
Bu durumda oluşturulan SQL biraz daha karmaşıktır:
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
Son olarak, yine ile ExecuteDelete
olduğu gibi filtre diğer tablolara başvurabilir. Örneğin, eski gönderilerden tüm etiketleri güncelleştirmek için:
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
Oluşturan:
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))
ve üzerinde ExecuteUpdate
daha fazla bilgi ve kod örnekleri için bkz. ExecuteUpdate ve ExecuteDelete.ExecuteDelete
Devralma ve birden çok tablo
ExecuteUpdate
ve ExecuteDelete
yalnızca tek bir tabloda işlem yapabilir. Bunun, farklı devralma eşleme stratejileriyle çalışırken etkileri vardır. Genellikle, değiştirebileceğiniz tek bir tablo olduğundan, TPH eşleme stratejisi kullanılırken herhangi bir sorun olmaz. Örneğin, tüm FeaturedPost
varlıkları silme:
await context.Set<FeaturedPost>().ExecuteDeleteAsync();
TPH eşlemesi kullanılırken aşağıdaki SQL'i oluşturur:
DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'
TPC eşleme stratejisi kullanılırken de bu durumla ilgili herhangi bir sorun yoktur, çünkü tek bir tabloda yalnızca değişiklik yapılması gerekir:
DELETE FROM [f]
FROM [FeaturedPosts] AS [f]
Ancak, TPT eşleme stratejisi kullanılırken bu girişim başarısız olur çünkü iki farklı tablodan satırların silinmesini gerektirir.
Sorguya filtre eklemek genellikle işlemin hem TPC hem de TPT stratejileriyle başarısız olacağı anlamına gelir. Bunun nedeni, satırların birden çok tablodan silinmesi gerekebileceğidir. Örneğin, şu sorgu:
await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();
TPH kullanırken aşağıdaki SQL'i oluşturur:
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%')
Ancak TPC veya TPT kullanılırken başarısız olur.
İpucu
Sorun #10879 , bu senaryolarda birden çok komutu otomatik olarak göndermek için destek eklemeyi izler. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.
ExecuteDelete
ve ilişkiler
Yukarıda belirtildiği gibi, bir ilişkinin sorumlusunun silinebilmesi için önce bağımlı varlıkların silinmesi veya güncelleştirilmesi gerekebilir. Örneğin, her Post
biri ilişkili Author
öğesine bağımlıdır. Bu, bir gönderi hala başvuruda bulunsa yazarın silinemeyeceği anlamına gelir; bunu yaptığınızda veritabanındaki yabancı anahtar kısıtlaması ihlal edilecek. Örneğin, bunu deneme:
await context.Authors.ExecuteDeleteAsync();
SQL Server'da aşağıdaki özel durumla sonuçlanır:
Microsoft.Data.SqlClient.SqlException (0x80131904): DELETE deyimi "FK_Posts_Authors_AuthorId" BAŞVURU kısıtlamasıyla çakıştı. Çakışma "TphBlogsContext" veritabanında, "dbo" tablosunda oluştu. Postlar", 'AuthorId' sütunu. Deyim sonlandırıldı.
Bunu düzeltmek için önce gönderileri silmemiz veya yabancı anahtar özelliğini null olarak ayarlayarak AuthorId
her gönderi ile yazarı arasındaki ilişkiyi kesmemiz gerekir. Örneğin, silme seçeneğini kullanarak:
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
İpucu
TagWith
normal sorguları etiketlemek ExecuteDelete
için veya ExecuteUpdate
aynı şekilde kullanılabilir.
Bu, iki ayrı komutla sonuç verir; bağımlıları ilk silecek olan:
-- Deleting posts...
DELETE FROM [p]
FROM [Posts] AS [p]
İkinci olarak da sorumluları silin:
-- Deleting authors...
DELETE FROM [a]
FROM [Authors] AS [a]
Önemli
Birden çok ExecuteDelete
ve ExecuteUpdate
komutu varsayılan olarak tek bir işlemde yer almayacaktır. Ancak DbContext işlem API'leri , bu komutları bir işlemde sarmalamanın normal yöntemiyle kullanılabilir.
İpucu
Bu komutları tek bir gidiş dönüşte göndermek Sorun #10879'a bağlıdır. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.
Veritabanında art arda silmeleri yapılandırmak burada çok yararlı olabilir. Modelimizde ve arasındaki Blog
Post
ilişki gereklidir ve bu da EF Core'un kurala göre art arda silme yapılandırmasına neden olur. Bu, bir blog veritabanından silindiğinde tüm bağımlı gönderilerinin de silineceği anlamına gelir. Ardından tüm blogları ve gönderileri silmek için yalnızca blogları silmemiz gerekir:
await context.Blogs.ExecuteDeleteAsync();
Bu, aşağıdaki SQL'e neden olur:
DELETE FROM [b]
FROM [Blogs] AS [b]
Bu, bir blogu silerken, yapılandırılan art arda silme işlemiyle ilgili tüm gönderilerin silinmesine de neden olur.
Daha Hızlı SaveChanges
EF7'de ve SaveChangesAsync performansı SaveChanges önemli ölçüde geliştirilmiştir. Bazı senaryolarda, değişiklikleri kaydetmek artık EF Core 6.0'dan dört kat daha hızlıdır!
Bu iyileştirmelerin çoğu şunlardan gelir:
- Veritabanına daha az gidiş dönüş gerçekleştirme
- Daha hızlı SQL oluşturma
Bu iyileştirmelerin bazı örnekleri aşağıda gösterilmiştir.
Not
Bu değişikliklerin ayrıntılı bir şekilde tartışılması için .NET Blogu'ndaki Entity Framework Core 7 Preview 6: Performance Edition Duyuruları bölümüne bakın.
İpucu
Burada gösterilen kod SaveChangesPerformanceSample.cs.
Gereksiz işlemler ortadan kaldırılıyor
Tüm modern ilişkisel veritabanları, tek sql deyimlerinin (çoğu) işlemselliğini garanti eder. Yani, bir hata oluşsa bile deyimi hiçbir zaman yalnızca kısmen tamamlanmaz. EF7, bu durumlarda açık bir işlem başlatmaktan kaçınıyor.
Örneğin, aşağıdaki çağrısı için günlüğe SaveChanges
bakarak:
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
EF Core 6.0'da komutun INSERT
bir işlemi başlatmak ve işlemek için komutlar tarafından sarmalandığını gösterir:
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, işlemin burada gerekli olmadığını algılar ve bu nedenle şu çağrıları kaldırır:
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);
Bu, özellikle veritabanına yapılan çağrıların gecikme süresi yüksek olduğunda genel performansta büyük fark yaratabilecek iki veritabanı gidiş dönüşlerini kaldırır. Tipik üretim sistemlerinde veritabanı, uygulamayla aynı makinede birlikte bulunmaz. Bu da gecikme süresinin genellikle nispeten yüksek olduğu ve bu iyileştirmenin özellikle gerçek dünyadaki üretim sistemlerinde etkili olduğu anlamına gelir.
Basit Kimlik ekleme için geliştirilmiş SQL
Yukarıdaki örnekte anahtar sütunu olan ve veritabanı tarafından oluşturulan başka değer içermeyen tek bir IDENTITY
satır eklenir. EF7, kullanarak OUTPUT INSERTED
BU durumda SQL'i basitleştirir. Bu basitleştirme diğer birçok durum için geçerli olmasa da, bu tür tek satırlı ekleme birçok uygulamada çok yaygın olduğu için geliştirmek yine de önemlidir.
Birden çok satır ekleme
EF Core 6.0'da, birden çok satır eklemeye yönelik varsayılan yaklaşım, tetikleyicileri olan tablolar için SQL Server desteğindeki sınırlamalar tarafından yönlendirildi. Tablolarında tetikleyicileri olan kullanıcıların azınlığı için bile varsayılan deneyimin çalıştığından emin olmak istedik. Bu, SQL Server'da tetikleyicilerle çalışmadığından basit OUTPUT
bir yan tümce kullanamadığımız anlamına geliyordu. Bunun yerine, birden çok varlık eklerken EF Core 6.0 bazı oldukça karışık SQL oluşturdu. Örneğin, bu çağrısı:SaveChanges
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
EF Core 6.0 ile SQL Server'da çalıştırıldığında aşağıdaki eylemlerle sonuçılır:
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.
Önemli
Bu karmaşık olsa da, bunun gibi birden çok eklemeyi toplu olarak çalıştırmak, her ekleme için tek bir komut göndermekten önemli ölçüde daha hızlıdır.
EF7'de, tablolarınız tetikleyici içeriyorsa bu SQL'i almaya devam edebilirsiniz, ancak genel durum için artık biraz karmaşıksa çok daha verimli komutlar oluşturuyoruz:
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;
Tek bir ekleme durumunda olduğu gibi işlem, örtük bir işlem tarafından korunan tek bir deyim olduğundan MERGE
yok olur. Ayrıca geçici tablo gitti ve OUTPUT yan tümcesi artık oluşturulan kimlikleri doğrudan istemciye geri gönderiyor. Bu, uygulama ve veritabanı arasındaki gecikme süresi gibi çevresel faktörlere bağlı olarak EF Core 6.0'dan dört kat daha hızlı olabilir.
Tetikleyiciler
Tabloda tetikleyiciler varsa yukarıdaki SaveChanges
kodda çağrısı bir özel durum oluşturur:
İşlenmeyen özel durum. Microsoft.EntityFrameworkCore.DbUpdateException:
Hedef tabloda veritabanı tetikleyicileri olduğundan değişiklikler kaydedilemedi. Lütfen varlık türünüzü uygun şekilde yapılandırın, daha fazla bilgi için bkzhttps://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers
.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
DML deyiminin 'BlogsWithTriggers' hedef tablosunda, deyimi INTO yan tümcesi olmayan bir OUTPUT yan tümcesi içeriyorsa etkin tetikleyici olamaz.
Aşağıdaki kod, EF Core'a tablonun tetikleyicisi olduğunu bildirmek için kullanılabilir:
modelBuilder
.Entity<BlogWithTrigger>()
.ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));
EF7, bu tablo için ekleme ve güncelleştirme komutları gönderirken EF Core 6.0 SQL'e geri döner.
Tüm eşlenen tabloları tetikleyicilerle otomatik olarak yapılandırma kuralı da dahil olmak üzere daha fazla bilgi için EF7 hataya neden olan değişiklikler belgelerinde tetikleyicileri olan SQL Server tabloları artık özel EF Core yapılandırması gerektiriyor bölümüne bakın.
Grafik eklemek için daha az gidiş dönüş
Yeni bir asıl varlık ve yeni sorumluya başvuran yabancı anahtarlara sahip yeni bağımlı varlıklar içeren varlıkların grafiğini eklemeyi göz önünde bulundurun. Örneğin:
await context.AddAsync(
new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();
Sorumlunun birincil anahtarı veritabanı tarafından oluşturulursa, sorumlu eklenene kadar bağımlıdaki yabancı anahtar için ayarlanacağı değer bilinmez. EF Core bunun için iki gidiş dönüş oluşturur: biri sorumluyu eklemek ve yeni birincil anahtarı geri almak için, diğeri ise yabancı anahtar değeri kümesine sahip bağımlıları eklemek için. Bunun için iki deyim olduğundan bir işlem gereklidir, yani toplam dört gidiş dönüş vardır:
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.
Ancak, bazı durumlarda, birincil anahtar değeri sorumlu eklenmeden önce bilinir. Buna aşağıdakiler dahildir:
- Otomatik olarak oluşturulmayan anahtar değerleri
- İstemcide oluşturulan anahtar değerleri( anahtarlar gibi Guid )
- Bir hi-lo değer oluşturucu kullanılırken olduğu gibi sunucuda toplu olarak oluşturulan anahtar değerleri
EF7'de bu durumlar artık tek gidiş dönüş olarak iyileştirilmiştir. Örneğin, SQL Server'da yukarıdaki örnekte birincil Blog.Id
anahtar hi-lo oluşturma stratejisini kullanacak şekilde yapılandırılabilir:
modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();
SaveChanges
Yukarıdaki çağrı artık eklemeler için tek bir gidiş dönüş için iyileştirilmiştir.
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.
Burada bir işlemin hala gerekli olduğuna dikkat edin. Bunun nedeni, eklemelerin iki ayrı tabloya dönüştürülmesidir.
EF7, EF Core 6.0'ın birden fazla oluşturacağı diğer durumlarda da tek bir toplu iş kullanır. Örneğin, aynı tabloya satır silerken ve eklerken.
SaveChanges değeri
Buradaki örneklerden bazılarının gösterdiği gibi, sonuçları veritabanına kaydetmek karmaşık bir iş olabilir. EF Core gibi bir şeyin kullanılması değerinin gerçekten gösterildiği yerdir. EF Core:
- Gidiş dönüşleri azaltmak için birden çok ekleme, güncelleştirme ve silme komutlarını birlikte toplu olarak işler
- Açık bir işlem gerekip gerekmediğini anlar
- Veritabanı kısıtlamalarının ihlal edilmemesi için varlıkların hangi sırada ekleneceğini, güncelleştirileceğini ve silineceğini belirler
- Veritabanı tarafından oluşturulan değerlerin verimli bir şekilde döndürülmesini ve varlıklara geri yayılmasını sağlar
- Birincil anahtarlar için oluşturulan değerleri kullanarak yabancı anahtar değerlerini otomatik olarak ayarlar
- Eşzamanlılık çakışmalarını algılama
Buna ek olarak, farklı veritabanı sistemleri bu durumların çoğu için farklı SQL gerektirir. EF Core veritabanı sağlayıcısı, her olay için doğru ve verimli komutların gönderilmesini sağlamak için EF Core ile birlikte çalışır.
Somut tür başına tablo (TPC) devralma eşlemesi
VARSAYıLAN olarak EF Core, .NET türlerinin devralma hiyerarşisini tek bir veritabanı tablosuyla eşler. Bu, hiyerarşi başına tablo (TPH) eşleme stratejisi olarak bilinir. EF Core 5.0, her .NET türünü farklı bir veritabanı tablosuna eşlemeyi destekleyen tür başına tablo (TPT) stratejisini kullanıma sunar. EF7, beton başına tablo türü (TPC) stratejisini tanıtır. TPC ayrıca .NET türlerini farklı tablolarla eşler, ancak TPT stratejisiyle ilgili bazı yaygın performans sorunlarını giderecek şekilde.
İpucu
Burada gösterilen kod TpcInheritanceSample.cs.
İpucu
EF Ekibi, .NET Veri Topluluğu Standup'ın bir bölümünde TPC eşlemesi hakkında ayrıntılı bir şekilde bilgi verdi ve konuştu. Tüm Topluluk Standup bölümlerinde olduğu gibi, TPC bölümünü artık YouTube'da izleyebilirsiniz.
TPC veritabanı şeması
TPC stratejisi TPT stratejisine benzer, ancak hiyerarşideki her somut tür için farklı bir tablo oluşturulur, ancak tablolar soyut türler için oluşturulmaz; bu nedenle "somut tür başına tablo" adıdır. TPT'de olduğu gibi, tablonun kendisi kaydedilen nesnenin türünü gösterir. Ancak, TPT eşlemesinin aksine, her tablo somut türdeki ve temel türlerindeki her özelliğin sütunlarını içerir. TPC veritabanı şemaları normal dışıdır.
Örneğin, şu hiyerarşiyi eşlemeyi göz önünde bulundurun:
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>"}";
}
SQL Server kullanılırken, bu hiyerarşi için oluşturulan tablolar şunlardır:
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]));
Şunlara dikkat edin:
Nesne modelinde
Animal
olduğundan veyaPet
türleriabstract
için tablo yoktur. C# öğesinin soyut türlerin örneklerine izin vermediğini ve bu nedenle soyut tür örneğinin veritabanına kaydedileceği bir durum olmadığını unutmayın.Temel türlerdeki özelliklerin eşlemesi her beton türü için yinelenir. Örneğin, her tablonun bir
Name
sütunu vardır ve hem Cats hem de Dogs'un birVet
sütunu vardır.Bazı verilerin bu veritabanına kaydedilmesi aşağıdaki sonuçlara neden olur:
Cats tablosu
Id | Veri Akışı Adı | FoodId | Veteriner | EducationLevel |
---|---|---|---|---|
1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA |
2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Okul öncesi |
8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Evcil Hayvan Hastanesi | Bsc |
Köpekler masası
Id | Veri Akışı Adı | FoodId | Veteriner | FavoriteToy |
---|---|---|---|---|
3 | Tost | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Bay Sincap |
FarmAnimals tablosu
Id | Veri Akışı Adı | FoodId | Değer | Türler |
---|---|---|---|---|
4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus |
İnsanlar tablosu
Id | Veri Akışı Adı | FoodId | FavoriteAnimalId |
---|---|---|---|
5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
9 | Katie | boş | 8 |
TPT eşlemesinin aksine, tek bir nesnenin tüm bilgilerinin tek bir tabloda yer aldığına dikkat edin. Ayrıca, TPH eşlemesinin aksine, model tarafından hiçbir zaman kullanılmayan herhangi bir tabloda sütun ve satır birleşimi yoktur. Aşağıda bu özelliklerin sorgular ve depolama için nasıl önemli olabileceğini göreceğiz.
TPC devralmayı yapılandırma
Devralma hiyerarşisindeki tüm türler, hiyerarşiYI EF Core ile eşlerken modele açıkça dahil edilmelidir. Bu, her tür için cihazınızda DbContext
özellikler oluşturularak DbSet
yapılabilir:
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>();
Veya içindeki yöntemini kullanarakEntity
:OnModelCreating
modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();
Önemli
Bu, türetilmiş eşlenmiş temel tür türlerinin aynı derlemede yer alıyorsa otomatik olarak bulunabileceği eski EF6 davranışından farklıdır.
Varsayılan strateji olduğundan hiyerarşiyi TPH olarak eşlemek için başka hiçbir şey yapılması gerekmez. Ancak EF7'den başlayarak TPH, hiyerarşinin temel türüne çağrılarak UseTphMappingStrategy
açık hale getirilebilir:
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
Bunun yerine TPT kullanmak için bunu olarak UseTptMappingStrategy
değiştirin:
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
Benzer şekilde, UseTpcMappingStrategy
TPC'yi yapılandırmak için kullanılır:
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
Her durumda, her tür için kullanılan tablo adı , üzerindeki özellik adından DbSet
alınır veya oluşturucu yöntemi veya [Table]
özniteliği kullanılarak ToTable
yapılandırılabilir.DbContext
TPC sorgu performansı
Sorgular için TPC stratejisi, belirli bir varlık örneğine ilişkin bilgilerin her zaman tek bir tabloda depolanmasını sağladığından TPT'ye göre bir geliştirmedir. Bu, eşlenen hiyerarşi büyük olduğunda ve her biri çok sayıda özelliğe sahip ve çoğu sorguda yalnızca küçük bir tür alt kümesinin kullanıldığı birçok somut (genellikle yaprak) türe sahip olduğunda TPC stratejisinin yararlı olabileceği anlamına gelir.
Üç basit LINQ sorgusu için oluşturulan SQL, TPH ve TPT ile karşılaştırıldığında TPC'nin nerede iyi çalıştığını gözlemlemek için kullanılabilir. Bu sorgular şunlardır:
Hiyerarşideki tüm türlerin varlıklarını döndüren sorgu:
context.Animals.ToList();
Hiyerarşideki türlerin bir alt kümesindeki varlıkları döndüren sorgu:
context.Pets.ToList();
Hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndüren sorgu:
context.Cats.ToList();
TPH sorguları
TPH kullanırken, üç sorgu da yalnızca tek bir tabloyu sorgular, ancak ayrımcı sütununda farklı filtreler vardır:
Hiyerarşideki tüm türlerin varlıklarını döndüren TPH SQL:
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]
Hiyerarşideki türlerin bir alt kümesinden varlıkları döndüren TPH SQL:
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, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel] FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Cat'
Tüm bu sorgular, özellikle ayırıcı sütununda uygun bir veritabanı diziniyle iyi performans göstermelidir.
TPT sorguları
TPT kullanılırken, belirli bir somut türe ait veriler birçok tabloya bölündüğünden, bu sorguların tümü birden çok tabloyu birleştirmeyi gerektirir:
Hiyerarşideki tüm türlerin varlıklarını döndüren TPT SQL:
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]
Hiyerarşideki türlerin bir alt kümesinden varlıkları döndüren TPT SQL:
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, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:
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]
Not
EF Core, verilerin hangi tablodan geldiğini ve bu nedenle kullanılacak doğru türü belirlemek için "ayrıştırıcı sentezi" kullanır. Bunun nedeni, LEFT JOIN'in doğru tür olmayan bağımlı kimlik sütunu ("alt tablolar") için null değer döndürmesidir. Bu nedenle bir [d].[Id]
köpek için null olmayan ve diğer tüm (somut) kimlikler null olacaktır.
Bu sorguların tümü, tablo birleştirmeleri nedeniyle performans sorunlarıyla karşılaşabilir. Bu nedenle TPT, sorgu performansı için hiçbir zaman iyi bir seçim değildir.
TPC sorguları
Sorgulanması gereken tablo sayısı azaldığından, TPC tüm bu sorgular için TPT'ye göre geliştirilir. Buna ek olarak, her tablodan elde edilen sonuçlar kullanılarak UNION ALL
birleştirilir. Bu, satırlar arasında herhangi bir eşleştirme gerçekleştirmesi veya satırların yinelenenleri kaldırması gerekmediğinden, tablo birleşiminden çok daha hızlı olabilir.
TPC SQL hiyerarşideki tüm türlerin varlıklarını döndüren:
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, hiyerarşideki türlerin bir alt kümesinden varlık döndüren:
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, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel] FROM [Cats] AS [c]
TPC, bu sorguların tümü için TPT'den daha iyi olsa da, birden çok türün örnekleri döndürülürken TPH sorguları hala daha iyidir. Bu, TPH'nin EF Core tarafından kullanılan varsayılan strateji olmasının nedenlerinden biridir.
3. sorgu için SQL'de gösterildiği gibi, TPC tek bir yaprak türündeki varlıkları sorgularken gerçekten çok başarılı olur. Sorgu yalnızca tek bir tablo kullanır ve filtreleme gerektirmez.
TPC eklemeleri ve güncelleştirmeleri
TPC, yeni bir varlığı kaydederken de iyi performans gösterir, çünkü bunun için tek bir tabloya yalnızca tek bir satır eklenmesi gerekir. Bu, TPH için de geçerlidir. TPT ile, daha az iyi performans gösteren birçok tabloya satır eklenmelidir.
Aynı durum genellikle güncelleştirmeler için de geçerlidir, ancak bu durumda güncelleştirilen tüm sütunlar TPT için bile aynı tablodaysa fark önemli olmayabilir.
Alanla ilgili dikkat edilmesi gerekenler
Hem TPT hem de TPC, genellikle kullanılmayan birçok özelliğe sahip çok sayıda alt tür olduğunda TPH'den daha az depolama alanı kullanabilir. Bunun nedeni, TPH tablosundaki her satırın bu kullanılmayan özelliklerin her biri için bir NULL
depolaması gerekir. Pratikte, bu nadiren bir sorundur, ancak bu özelliklere sahip büyük miktarda veriyi depolarken göz önünde bulundurmaya değer.
İpucu
Veritabanı sisteminiz destekliyorsa (e.g. SQL Sunucusu), nadiren doldurulacak TPH sütunları için "seyrek sütunlar" kullanmayı göz önünde bulundurun.
Anahtar oluşturma
Seçilen devralma eşleme stratejisi, birincil anahtar değerlerinin nasıl oluşturulup yönetildiğinin sonuçlarına neden olur. TPH'deki anahtarlar kolaydır çünkü her varlık örneği tek bir tabloda tek bir satırla temsil edilir. Herhangi bir tür anahtar değeri oluşturma kullanılabilir ve ek kısıtlama gerekmez.
TPT stratejisi için, tabloda her zaman hiyerarşinin temel türüne eşlenmiş bir satır vardır. Bu satırda herhangi bir tür anahtar oluşturma kullanılabilir ve diğer tabloların anahtarları yabancı anahtar kısıtlamaları kullanılarak bu tabloya bağlanır.
TPC için işler biraz daha karmaşık hale geliyor. İlk olarak, EF Core'un, varlıkların farklı türleri olsa bile bir hiyerarşideki tüm varlıkların benzersiz bir anahtar değerine sahip olmasını gerektirdiğini anlamak önemlidir. Bu nedenle, örnek modelimizi kullanarak bir Köpek, Kedi ile aynı Kimlik anahtarı değerine sahip olamaz. İkincisi, TPT'nin aksine, anahtar değerlerin yaşadığı ve oluşturulabileceği tek bir yer olarak davranabilen ortak bir tablo yoktur. Bu, basit Identity
bir sütunun kullanılamayacağı anlamına gelir.
Sıraları destekleyen veritabanları için anahtar değerleri, her tablo için varsayılan kısıtlamada başvurulan tek bir sıra kullanılarak oluşturulabilir. Bu, yukarıda gösterilen TPC tablolarında kullanılan ve her tablonun aşağıdakilere sahip olduğu stratejidir:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence
, EF Core tarafından oluşturulan bir veritabanı dizisidir. Bu strateji, SQL Server için EF Core veritabanı sağlayıcısı kullanılırken varsayılan olarak TPC hiyerarşileri için kullanılır. Sıraları destekleyen diğer veritabanları için veritabanı sağlayıcıları da benzer bir varsayılan değere sahip olmalıdır. Hi-Lo desenleri gibi dizileri kullanan diğer anahtar oluşturma stratejileri de TPC ile kullanılabilir.
Standart Kimlik sütunları TPC ile çalışmaz, ancak her tablo uygun bir tohumla yapılandırılırsa ve her tablo için oluşturulan değerler hiçbir zaman çakışmayacak şekilde artırılırsa Kimlik sütunlarını kullanmak mümkündür. Örneğin:
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 dizileri veya Kimlik çekirdek/artışını desteklemez ve bu nedenle TPC stratejisiyle SQLite kullanılırken tamsayı anahtar değeri oluşturma desteklenmez. Ancak, istemci tarafı oluşturma veya genel olarak benzersiz anahtarlar (örneğin, GUID anahtarları) SQLite dahil olmak üzere tüm veritabanlarında desteklenir.
Yabancı anahtar kısıtlamaları
TPC eşleme stratejisi, normal olmayan bir SQL şeması oluşturur. Bu, bazı veritabanı temizleyicilerinin buna karşı olmasının bir nedenidir. Örneğin, yabancı anahtar sütununu FavoriteAnimalId
göz önünde bulundurun. Bu sütundaki değer, bir hayvanın birincil anahtar değeriyle eşleşmelidir. Bu, TPH veya TPT kullanılırken veritabanında basit bir FK kısıtlaması ile zorunlu kılınabilir. Örneğin:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Ancak TPC kullanırken, bir hayvanın birincil anahtarı, o hayvanın beton türü için tabloda depolanır. Örneğin, bir kedinin Cats.Id
birincil anahtarı sütunda depolanırken, köpeğin birincil anahtarı sütunda Dogs.Id
depolanır ve bu şekilde devam edilir. Bu, bu ilişki için bir FK kısıtlaması oluşturulamayacağı anlamına gelir.
Uygulamada, uygulama geçersiz veri eklemeye çalışmadığı sürece bu bir sorun değildir. Örneğin, tüm veriler EF Core tarafından eklenirse ve varlıkları ilişkilendirmek için gezintileri kullanırsa, FK sütununun her zaman geçerli PK değeri içermesi garanti edilir.
Özet ve rehberlik
Özetle, TPC, kodunuz çoğunlukla tek bir yaprak türündeki varlıkları sorguladığında kullanmak için iyi bir eşleme stratejisidir. Bunun nedeni depolama gereksinimlerinin daha küçük olması ve dizin gerektirebilecek ayrıştırıcı sütun olmamasıdır. Eklemeler ve güncelleştirmeler de verimlidir.
Bununla birlikte, TPH çoğu uygulama için genellikle uygundur ve çok çeşitli senaryolar için iyi bir varsayılandır, bu nedenle ihtiyacınız yoksa TPC'nin karmaşıklığını eklemeyin. Özel olarak, kodunuz çoğunlukla temel türe göre sorgu yazma gibi birçok türdeki varlıkları sorgulayacaksa, TPC üzerinden TPH'ye doğru eğilin.
TPT'yi yalnızca dış etmenler tarafından kısıtlanmışsa kullanın.
Özel Tersine Mühendislik Şablonları
Artık veritabanından ef modelini tersine mühendislik yaparken iskelesi oluşturulmuş kodu özelleştirebilirsiniz. Projenize varsayılan şablonları ekleyerek başlayın:
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
Şablonlar daha sonra özelleştirilebilir ve ve Scaffold-DbContext
tarafından dotnet ef dbcontext scaffold
otomatik olarak kullanılır.
Daha fazla ayrıntı için bkz . Özel Tersine Mühendislik Şablonları.
İpucu
EF Ekibi, .NET Veri Topluluğu Standup'ın bir bölümünde tersine mühendislik şablonlarını ayrıntılı bir şekilde gösterdi ve konuştu. Tüm Topluluk Standup bölümlerinde olduğu gibi, T4 şablonları bölümünü artık YouTube'da izleyebilirsiniz.
Model oluşturma kuralları
EF Core, uygulamanın varlık türlerinin temel alınan veritabanına nasıl eşlenmiş olduğunu açıklamak için bir meta veri "modeli" kullanır. Bu model yaklaşık 60 "kural" kümesi kullanılarak oluşturulur. Kurallar tarafından oluşturulan model daha sonra eşleme öznitelikleri ("veri ek açıklamaları" olarak adlandırılır) ve/veya içindekiOnModelCreating
API çağrıları DbModelBuilder
kullanılarak özelleştirilebilir.
EF7'den başlayarak, uygulamalar artık bu kuralları kaldırabilir veya değiştirebilir ve yeni kurallar ekleyebilir. Model oluşturma kuralları, model yapılandırmasını denetlemenin güçlü bir yoludur, ancak karmaşık ve düzeltilmesi zor olabilir. Çoğu durumda, özellikler ve türler için ortak bir yapılandırmayı kolayca belirtmek için mevcut kural öncesi model yapılandırması kullanılabilir.
tarafından DbContext
kullanılan kurallarda değişiklikler yöntemi geçersiz kılınarak DbContext.ConfigureConventions
yapılır. Örneğin:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
İpucu
Tüm yerleşik model oluşturma kurallarını bulmak için arabirimini uygulayan IConvention her sınıfı arayın.
İpucu
Burada gösterilen kod ModelBuildingConventionsSample.cs gelir.
Mevcut bir kuralı kaldırma
Bazen yerleşik kurallardan biri uygulamanız için uygun olmayabilir ve bu durumda kaldırılabilir.
Örnek: Yabancı anahtar sütunları için dizin oluşturma
Genellikle yabancı anahtar (FK) sütunları için dizinler oluşturmak mantıklıdır ve bu nedenle bunun için yerleşik bir kural vardır: ForeignKeyIndexConvention. ve Author
ile ilişkileri olan bir Post
varlık türünün model hata ayıklama görünümüne Blog
baktığımızda biri FK, diğeri de FK için BlogId
olmak üzere iki dizinin oluşturulduğunu AuthorId
görebiliriz.
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
Ancak dizinlerin ek yükü vardır ve burada istendiği gibi, bunları tüm FK sütunları için oluşturmak her zaman uygun olmayabilir. Bunu başarmak için, ForeignKeyIndexConvention
modeli oluştururken kaldırılabilir:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Modelin şimdilik hata ayıklama görünümüne Post
baktığımızda, FK'lerdeki dizinlerin oluşturulmadığını görüyoruz:
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
İstendiğinde, dizinler yine de yabancı anahtar sütunları IndexAttributeiçin kullanılarak açıkça oluşturulabilir:
[Index("BlogId")]
public class Post
{
// ...
}
Veya içinde yapılandırma ile OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}
Varlık türüne Post
yeniden baktığınızda artık dizini içeriyor ancak dizini içermiyor BlogId
AuthorId
:
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
İpucu
Modeliniz yapılandırma için eşleme özniteliklerini (yani veri ek açıklamalarını) kullanmıyorsa, model oluşturmayı hızlandırmak için ile AttributeConvention
biten tüm kurallar güvenle kaldırılabilir.
Yeni kural ekleme
Mevcut kuralları kaldırmak bir başlangıçtır, ancak tamamen yeni model oluşturma kuralları eklemeye ne dersin? EF7 bunu da destekliyor!
Örnek: Ayrımcı özelliklerin kısıt uzunluğu
Hiyerarşi başına tablo devralma eşleme stratejisi , belirli bir satırda hangi türün gösterileceğini belirtmek için ayrıştırıcı sütun gerektirir. Varsayılan olarak, EF ayırıcı için herhangi bir ayrımcı uzunluğu için çalışmasını sağlayan ilişkisiz bir dize sütunu kullanır. Ancak, ayrımcı dizelerin uzunluk üst sınırının kısıtlanması, depolama ve sorguların daha verimli olmasını sağlayabilir. Şimdi bunu yapacak yeni bir kural oluşturalım.
EF Core model oluşturma kuralları, model oluşturulurken modelde yapılan değişikliklere bağlı olarak tetiklenir. Bu, açık yapılandırma yapıldıkçe, eşleme öznitelikleri uygulandıkça ve diğer kurallar çalıştırıldığında modelin güncel kalmasını sağlar. Buna katılmak için her kural, kuralın ne zaman tetikleneceğini belirleyen bir veya daha fazla arabirim uygular. Örneğin, modele yeni bir varlık türü eklendiğinde uygulayan IEntityTypeAddedConvention bir kural tetiklenir. Benzer şekilde, hem uygulayan hem IKeyAddedConvention de IForeignKeyAddedConvention modele bir anahtar veya yabancı anahtar eklendiğinde tetiklenen bir kural.
Modele bir noktada yapılan yapılandırma daha sonraki bir noktada değiştirilebileceği veya kaldırılabildiği için uygulanacak arabirimleri bilmek karmaşık olabilir. Örneğin, bir anahtar kural tarafından oluşturulabilir, ancak daha sonra farklı bir anahtar açıkça yapılandırıldığında değiştirilir.
Ayırıcı uzunluk kuralını uygulamaya yönelik ilk denemeyi yaparak bunu biraz daha somut hale getirelim:
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);
}
}
}
Bu kural, bir varlık türü için eşlenmiş devralma hiyerarşisi her değiştirildiğinde tetikleneceği anlamına gelen öğesini uygular IEntityTypeBaseTypeChangedConvention. Kural daha sonra hiyerarşi için dize ayırıcı özelliğini bulur ve yapılandırılır.
Bu kural daha sonra içinde ConfigureConventions
çağrılarak Add
kullanılır:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DiscriminatorLengthConvention1());
}
İpucu
yöntemi, kuralın bir örneğini doğrudan eklemek yerine kuralın Add
örneklerini oluşturmak için bir fabrikayı kabul eder. Bu, kuralın EF Core iç hizmet sağlayıcısından bağımlılıkları kullanmasına olanak tanır. Bu kuralın bağımlılıkları olmadığından, hizmet sağlayıcısı parametresi hiçbir zaman kullanılmadığını belirten olarak adlandırılır _
.
Modeli oluşturmak ve varlık türüne Post
bakmak bunun işe yaradığını gösterir; ayırıcı özelliği artık en fazla 24 uzunlukla yapılandırılır:
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Ancak şimdi açıkça farklı bir ayrımcı özelliği yapılandırırsak ne olur? Örneğin:
modelBuilder.Entity<Post>()
.HasDiscriminator<string>("PostTypeDiscriminator")
.HasValue<Post>("Post")
.HasValue<FeaturedPost>("Featured");
Modelin hata ayıklama görünümüne baktığımızda, ayrıştırıcı uzunluğunun artık yapılandırılmadığını fark ediyoruz!
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw
Bunun nedeni, kuralımızda yapılandırdığımız ayırıcı özelliğinin daha sonra özel ayrıştırıcı eklendiğinde kaldırılmış olmasıdır. Ayrıştırıcı değişikliklere tepki vermek için kuralımızda başka bir arabirim uygulayarak bunu düzeltmeye çalışabiliriz, ancak hangi arabirimi uygulayacağımızı bulmak kolay değildir.
Neyse ki, bu yaklaşımın işleri çok daha kolay hale getiren farklı bir yolu vardır. Çoğu zaman, son model doğru olduğu sürece modelin oluşturulurken nasıl göründüğü önemli değildir. Buna ek olarak, uygulamak istediğimiz yapılandırmanın genellikle tepki vermek için başka kuralları tetiklemesi gerekmez. Bu nedenle, kuralımız uygulamasını uygulayabilir IModelFinalizingConvention. Model sonlandırma kuralları, diğer tüm model oluşturma işlemleri tamamlandıktan sonra çalıştırılır ve bu nedenle modelin son durumuna erişim elde edilir. Model sonlandırma kuralı genellikle model öğelerini yapılandıran modelin tamamı üzerinde yinelenir. Bu durumda modeldeki tüm ayrımcıları bulup yapılandıracağız:
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);
}
}
}
}
Modeli bu yeni kuralla derledikten sonra, ayrıştırıcı uzunluğunun özelleştirildiği halde doğru yapılandırıldığını fark ettik:
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Yalnızca eğlence için bir adım ileri gidelim ve maksimum uzunluğu en uzun ayırıcı değerin uzunluğu olacak şekilde yapılandıralım.
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);
}
}
}
}
Artık en uzun ayırıcı sütun uzunluğu 8'dir ve bu da kullanımdaki en uzun ayrıştırıcı değeri olan "Öne Çıkan" uzunluğudur.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
İpucu
Kuralın ayırıcı sütun için de bir dizin oluşturması gerekip gerekmediğini merak ediyor olabilirsiniz. GitHub'da bu konu hakkında bir tartışma vardır. Kısa bir yanıt, bazen bir dizinin yararlı olabileceği, ancak çoğu zaman büyük olasılıkla işe yaramayacağıdır. Bu nedenle, her zaman bunu yapmak için bir kurala sahip olmak yerine burada gerektiği gibi uygun dizinler oluşturmak en iyisidir. Ancak buna katılmıyorsanız yukarıdaki kural da kolayca değiştirilip bir dizin oluşturulabilir.
Örnek: Tüm dize özellikleri için varsayılan uzunluk
Bu kez GitHub'da istendiği gibi herhangi bir dize özelliği için varsayılan en uzun uzunluğu ayarlayan bir sonlandırma kuralının kullanılabildiği başka bir örneğe göz atalım. Kural önceki örneğe oldukça benzer:
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);
}
}
}
Bu kural oldukça basittir. Modeldeki her dize özelliğini bulur ve maksimum uzunluğunu 512 olarak ayarlar. özelliklerinde Post
hata ayıklama görünümüne baktığımızda, tüm dize özelliklerinin artık en fazla 512 uzunluğuna sahip olduğunu görüyoruz.
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
Ancak özellik muhtemelen 512'den fazla karaktere izin vermelidir, aksi takdirde tüm gönderilerimiz oldukça kısa olacaktır! Bu, eşleme özniteliği kullanılarak yalnızca bu özellik için en uzun uzunluğu açıkça yapılandırarak kuralımızı değiştirmeden yapılabilir:
[MaxLength(4000)]
public string Content { get; set; }
Veya içindeki OnModelCreating
kodla:
modelBuilder.Entity<Post>()
.Property(post => post.Content)
.HasMaxLength(4000);
Artık 4000 ile açıkça yapılandırılan özellikler dışında Content
tüm özelliklerin uzunluğu en fazla 512 olur:
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)
Peki kuralımız neden açıkça yapılandırılan maksimum uzunluğu geçersiz kılmadı? Yanıt, EF Core'un her yapılandırma parçasının nasıl yapıldığını izlemesidir. Bu, sabit listesi tarafından ConfigurationSource temsil edilir. Farklı yapılandırma türleri şunlardır:
Explicit
: Model öğesi açıkça içinde yapılandırıldıOnModelCreating
DataAnnotation
: Model öğesi CLR türünde eşleme özniteliği (veri ek açıklaması) kullanılarak yapılandırıldıConvention
: Model öğesi bir model oluşturma kuralı tarafından yapılandırıldı
Kurallar, veya Explicit
olarak DataAnnotation
işaretlenmiş yapılandırmayı hiçbir zaman geçersiz kılmaz. Bu, örneğin özelliğinden Builder elde edilen bir "kural oluşturucusu" IConventionPropertyBuilderkullanılarak elde edilir. Örneğin:
property.Builder.HasMaxLength(512);
Kural oluşturucusunda çağrılmasıHasMaxLength
, yalnızca bir eşleme özniteliği tarafından veya içinde OnModelCreating
yapılandırılmamışsa maksimum uzunluğu ayarlar.
Bunun gibi oluşturucu yöntemlerinin ikinci bir parametresi de vardır: fromDataAnnotation
. Kural bir eşleme özniteliği adına yapılandırma yapıyorsa bunu true
olarak ayarlayın. Örneğin:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
Bu, değerini olarak ConfigurationSource
DataAnnotation
ayarlar. Bu, değerin artık üzerinde OnModelCreating
açık eşleme yoluyla geçersiz kılınabileceği, ancak eşleme dışı öznitelik kuralları tarafından geçersiz kılınabileceği anlamına gelir.
Son olarak, bu örnekten MaxStringLengthConvention
ayrılmadan önce hem hem DiscriminatorLengthConvention3
de aynı anda kullanırsak ne olur? Bunun yanıtı, model sonlandırma kuralları eklendikleri sırayla çalıştırıldığından, bunların hangi sırayla eklendiğine bağlı olmasıdır. Bu nedenle, en son eklenirse MaxStringLengthConvention
, en son çalışır ve ayırıcı özelliğinin maksimum uzunluğunu 512 olarak ayarlar. Bu nedenle, bu durumda, diğer tüm dize özelliklerini 512 olarak bırakırken yalnızca ayırıcı özellikleri için varsayılan maksimum uzunluğu geçersiz kılabilmesi için last eklemek DiscriminatorLengthConvention3
daha iyidir.
Mevcut bir kuralı değiştirme
Bazen mevcut bir kuralı tamamen kaldırmak yerine temelde aynı şeyi ancak değişen davranışı olan bir kuralla değiştirmek istiyoruz. Mevcut kural, uygun şekilde tetiklenmesi için gereken arabirimleri zaten uygulayacağından bu yararlı olur.
Örnek: Özellik eşlemeyi kabul etme
EF Core, tüm genel okuma-yazma özelliklerini kurala göre eşler. Bu , varlık türlerinizin tanımlanma biçimi için uygun olmayabilir. Bunu değiştirmek için, açıkça ile eşlenmediği OnModelCreating
veya adlı Persist
yeni bir öznitelikle işaretlenmediği sürece öğesini herhangi bir özelliği eşlemeyen kendi uygulamamızla değiştirebilirizPropertyDiscoveryConvention
:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}
Yeni kural şu şekildedir:
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;
}
}
}
}
İpucu
Yerleşik bir kuralı değiştirirken, yeni kural uygulaması mevcut kural sınıfından devralmalıdır. Bazı kuralların ilişkisel veya sağlayıcıya özgü uygulamaları olduğunu unutmayın. Bu durumda, yeni kural uygulaması kullanımdaki veritabanı sağlayıcısı için en belirli mevcut kural sınıfından devralmalıdır.
Kural daha sonra içindeki ConfigureConventions
yöntemi kullanılarak Replace
kaydedilir:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}
İpucu
Bu durum, mevcut kuralın bağımlılık nesnesiyle ProviderConventionSetBuilderDependencies
temsil edilen bağımlılıklara sahip olduğu bir durumdur. Bunlar kullanılarak GetRequiredService
iç hizmet sağlayıcısından alınır ve kural oluşturucusna geçirilir.
Bu kural, verilen varlık türünden tüm okunabilir özellikleri ve alanları alarak çalışır. Üye ile [Persist]
ilişkilendirildiyse, çağrılarak eşlenir:
entityTypeBuilder.Property(memberInfo);
Öte yandan, üye başka türlü eşlenmiş bir özellikse, aşağıdakiler kullanılarak modelden dışlanır:
entityTypeBuilder.Ignore(propertyInfo.Name);
Bu kuralın, ile [Persist]
işaretlendikleri sürece alanların eşlenmesine (özelliklere ek olarak) izin verdiğine dikkat edin. Bu, özel alanları modelde gizli anahtarlar olarak kullanabileceğimiz anlamına gelir.
Örneğin, aşağıdaki varlık türlerini göz önünde bulundurun:
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; }
}
Bu varlık türlerinden oluşturulan model:
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
Normalde IsClean
eşlenmiş olabileceğine dikkat edin, ancak ile [Persist]
işaretlenmediğinden (muhtemelen temizlik çamaşırların kalıcı bir özelliği olmadığından), artık eşlenmemiş bir özellik olarak kabul edilir.
İpucu
Bir özelliği eşlemek eşlenen özelliği daha fazla yapılandırmak için başka birçok kuralın çalıştırılmasını tetiklediğinden, bu kural model sonlandırma kuralı olarak uygulanamadı.
Saklı yordam eşlemesi
Varsayılan olarak, EF Core doğrudan tablolar veya güncelleştirilebilir görünümlerle çalışan ekleme, güncelleştirme ve silme komutları oluşturur. EF7, bu komutların saklı yordamlara eşlemini destekler.
İpucu
EF Core saklı yordamlar aracılığıyla sorgulamayı her zaman desteklemiştir. EF7'deki yeni destek açıkça eklemeler, güncelleştirmeler ve silmeler için saklı yordamları kullanma hakkındadır.
Önemli
Saklı yordam eşleme desteği, saklı yordamların önerileceği anlamına gelmez.
Saklı yordamlar , ve UpdateUsingStoredProcedure
DeleteUsingStoredProcedure
kullanılarak InsertUsingStoredProcedure
ile OnModelCreating
eşlenir. Örneğin, bir Person
varlık türü için saklı yordamları eşlemek için:
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();
});
Bu yapılandırma, SQL Server kullanırken aşağıdaki saklı yordamlarla eşler:
Eklemeler için
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
Güncelleştirmeler için
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
Silme işlemleri için
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
İpucu
Saklı yordamların modeldeki her tür için veya belirli bir türdeki tüm işlemler için kullanılması gerekmez. Örneğin, yalnızca DeleteUsingStoredProcedure
belirli bir tür için belirtilmişse, EF Core ekleme ve güncelleştirme işlemleri için normal SQL oluşturur ve yalnızca silme işlemleri için saklı yordamı kullanır.
Her yönteme geçirilen ilk bağımsız değişken saklı yordam adıdır. Bu atlanabilir, bu durumda EF Core "_Insert", "_Update" veya "_Delete" ile eklenen tablo adını kullanır. Bu nedenle yukarıdaki örnekte tablo "Kişiler" olarak adlandırıldığından saklı yordam adları işlev değişikliği yapılmadan kaldırılabilir.
İkinci bağımsız değişken, parametreler, dönüş değerleri ve sonuç sütunları da dahil olmak üzere saklı yordamın giriş ve çıkışını yapılandırmak için kullanılan bir oluşturucudur.
Parametreler
Parametreler, saklı yordam tanımında göründükleri sırada oluşturucuya eklenmelidir.
Not
Parametreler adlandırılabilir, ancak EF Core her zaman adlandırılmış bağımsız değişkenler yerine konumsal bağımsız değişkenler kullanarak saklı yordamları çağırır. Adla çağırmak ilginizi çekiyorsa, çağırma için parametre adlarını kullanmak üzere sproc eşlemesini yapılandırmaya izin ver için oy verin.
Her parametre oluşturucu yönteminin ilk bağımsız değişkeni, parametrenin bağlı olduğu modeldeki özelliği belirtir. Bu bir lambda ifadesi olabilir:
storedProcedureBuilder.HasParameter(a => a.Name);
Veya özellikle gölge özellikleri eşlerken yararlı olan bir dize:
storedProcedureBuilder.HasParameter("Name");
Parametreler varsayılan olarak "giriş" için yapılandırılır. "Output" veya "input/output" parametreleri iç içe oluşturucu kullanılarak yapılandırılabilir. Örneğin:
storedProcedureBuilder.HasParameter(
document => document.RetrievedOn,
parameterBuilder => parameterBuilder.IsOutput());
Farklı parametre türleri için üç farklı oluşturucu yöntemi vardır:
HasParameter
verilen özelliğin geçerli değerine bağlı normal bir parametre belirtir.HasOriginalValueParameter
verilen özelliğin özgün değerine bağlı bir parametre belirtir. Özgün değer, biliniyorsa, özelliğin veritabanından sorgulandığında sahip olduğu değerdir. Bu değer bilinmiyorsa, bunun yerine geçerli değer kullanılır. Özgün değer parametreleri eşzamanlılık belirteçleri için kullanışlıdır.HasRowsAffectedParameter
saklı yordamdan etkilenen satır sayısını döndürmek için kullanılan bir parametreyi belirtir.
İpucu
"Update" ve "delete" saklı yordamlarındaki anahtar değerleri için özgün değer parametreleri kullanılmalıdır. Bu, değiştirilebilir anahtar değerlerini destekleyen EF Core'un gelecek sürümlerinde doğru satırın güncelleştirilmesini sağlar.
Değer döndürme
EF Core, saklı yordamlardan değer döndürmek için üç mekanizmayı destekler:
- Yukarıda gösterildiği gibi çıkış parametreleri.
- Oluşturucu yöntemi kullanılarak
HasResultColumn
belirtilen sonuç sütunları. - Döndürülen değer, etkilenen satır sayısını döndürmeyle sınırlıdır ve oluşturucu yöntemi kullanılarak
HasRowsAffectedReturnValue
belirtilir.
Saklı yordamlardan döndürülen değerler genellikle anahtar veya hesaplanan sütun gibi Identity
oluşturulan, varsayılan veya hesaplanan değerler için kullanılır. Örneğin, aşağıdaki yapılandırma dört sonuç sütunu belirtir:
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);
});
Bunlar şunları döndürmek için kullanılır:
- Özelliği için
Id
oluşturulan anahtar değeri. - Özelliği için
FirstRecordedOn
veritabanı tarafından oluşturulan varsayılan değer. - özelliği için
RetrievedOn
veritabanı tarafından oluşturulan hesaplanan değer. - Özelliği için otomatik olarak oluşturulan
rowversion
eşzamanlılık belirteciRowVersion
.
Bu yapılandırma, SQL Server kullanırken aşağıdaki saklı yordamla eşler:
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
İyimser eşzamanlılık
İyimser eşzamanlılık saklı yordamlarda olduğu gibi çalışır. Saklı yordam şu şekilde olmalıdır:
- Satırın yalnızca geçerli bir belirteci varsa güncelleştirildiğinden emin olmak için yan
WHERE
tümcesinde eşzamanlılık belirteci kullanın. Eşzamanlılık belirteci için kullanılan değer genellikle eşzamanlılık belirteci özelliğinin özgün değeridir ancak olması gerekmez. - ETKILENEN satır sayısını döndürerek EF Core'un bunu etkilenen beklenen satır sayısıyla karşılaştırabilmesini ve değerler eşleşmiyorsa bir
DbUpdateConcurrencyException
atabilmesini sağlayın.
Örneğin, aşağıdaki SQL Server saklı yordamı otomatik eşzamanlılık rowversion
belirteci kullanır:
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
Bu, EF Core'da aşağıdakiler kullanılarak yapılandırılır:
.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();
});
Şunlara dikkat edin:
- Eşzamanlılık belirtecinin
RowVersion
özgün değeri kullanılır. - Saklı yordam, satırın yalnızca özgün değer eşleşiyorsa
RowVersion
güncelleştirildiğinden emin olmak için birWHERE
yan tümcesi kullanır. - için
RowVersion
oluşturulan yeni değer geçici bir tabloya eklenir. - Etkilenen (
@@ROWCOUNT
) satır sayısı ve oluşturulanRowVersion
değer döndürülür.
Devralma hiyerarşilerini saklı yordamlara eşleme
EF Core, saklı yordamların hiyerarşideki türler için tablo düzenini izlemesini gerektirir. Bu şu anlama gelir:
- TPH kullanılarak eşlenen hiyerarşide, eşlenen tek tabloyu hedefleyen tek bir ekleme, güncelleştirme ve/veya silme saklı yordamı olmalıdır. Ekleme ve güncelleştirme saklı yordamlarının ayrıştırıcı değeri için bir parametresi olmalıdır.
- TPT kullanılarak eşlenen hiyerarşi, soyut türler de dahil olmak üzere her tür için bir ekleme, güncelleştirme ve/veya silme yordamına sahip olmalıdır. EF Core, tüm tablolardaki satırları güncelleştirmek, eklemek ve silmek için gerektiğinde birden çok çağrı yapar.
- TPC kullanılarak eşlenen hiyerarşinin her somut tür için ekleme, güncelleştirme ve/veya silme saklı yordamı olmalıdır, ancak soyut türler olmamalıdır.
Not
Eşleme stratejisinden bağımsız olarak somut tür başına tek bir saklı yordam kullanmak ilgilendiğiniz bir şeyse devralma eşleme stratejisinden bağımsız olarak somut tür başına tek bir sproc kullanarak Destek'e oy verin.
Sahip olunan türleri saklı yordamlarla eşleme
Sahip olunan türler için saklı yordamların yapılandırması, iç içe sahip olunan tür oluşturucusunda yapılır. Örneğin:
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();
});
});
Not
Şu anda yalnızca sahip olunan ekleme, güncelleştirme ve silme desteği türleri için saklı yordamlar ayrı tablolara eşlenmelidir. Başka bir ifadeyle, sahip olunan tür sahip tablosundaki sütunlar tarafından temsil edilemez. Kaldırıldığını görmek istediğiniz bir sınırlamaysa CUD sproc eşlemesine "tablo" bölme desteği ekleme için oy verin.
Çoka çok birleştirme varlıklarını saklı yordamlarla eşleme
Çoka çok birleştirme varlıklarının saklı yordamlarının yapılandırılması, çoka çok yapılandırmasının bir parçası olarak gerçekleştirilebilir. Örneğin:
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();
});
});
});
Yeni ve geliştirilmiş kesme çizgileri ve olaylar
EF Core kesme makineleri EF Core işlemlerinin kesilmesini, değiştirilmesini ve/veya gizlenmesini sağlar. EF Core ayrıca geleneksel .NET olaylarını ve günlüğe kaydetmeyi de içerir.
EF7, kesiciler için aşağıdaki geliştirmeleri içerir:
- Yeni varlık örnekleri oluşturma ve doldurmaya yönelik kesme (diğer bir deyişle "gerçekleştirme")
- Sorgu derilmeden önce LINQ ifade ağacını değiştirmek için kesme
- İyimser eşzamanlılık işleme için kesme (
DbUpdateConcurrencyException
) - bağlantı dizesi ayarlandığını denetlemeden önce bağlantılar için kesme
- EF Core'un bir sonuç kümesini tüketmeyi bitirdiği ancak sonuç kümesinin kapatılmadan önce kesilme durumu
- EF Core ile oluşturma
DbConnection
için kesme - Başlatıldıktan sonra için
DbCommand
kesme
Buna ek olarak, EF7 aşağıdakiler için yeni geleneksel .NET olayları içerir:
- Bir varlık izlenmek veya durumu değiştirmek üzereyken, ancak gerçekten izlenmeden veya durumu değiştirmeden önce
- EF Core varlıklarda ve özelliklerde yapılan değişiklikleri algıladıktan önce ve sonra (kesme noktası olarak da bilinir
DetectChanges
)
Aşağıdaki bölümlerde, bu yeni kesme noktası özelliklerini kullanmanın bazı örnekleri gösterilmektedir.
Varlık oluşturmayla ilgili basit eylemler
İpucu
Burada gösterilen kod SimpleMaterializationSample.cs...
Yeni IMaterializationInterceptor , bir varlık örneği oluşturulmadan önce ve sonra ve bu örneğin özellikleri başlatılmadan önce ve sonra kesmeyi destekler. Kesme noktası her noktada varlık örneğini değiştirebilir veya değiştirebilir. Bu, aşağıdakileri sağlar:
- Doğrulama, hesaplanan değerler veya bayraklar için gereken eşlenmemiş özellikleri veya çağırma yöntemlerini ayarlama.
- Örnek oluşturmak için fabrika kullanma.
- EF'den farklı bir varlık örneği oluşturmak normalde önbellekten veya ara sunucu türünde bir örnek gibi bir örnek oluşturur.
- Bir varlık örneğine hizmet ekleme.
Örneğin, bir varlığın veritabanından alındığı zamanı takip etmek istediğimizi ve bu sayede verileri düzenleyen bir kullanıcıya görüntülenebileceğini düşünün. Bunu başarmak için önce bir arabirim tanımlayacağız:
public interface IHasRetrieved
{
DateTime Retrieved { get; set; }
}
Bir arabirim kullanmak, aynı kesme noktasının birçok farklı varlık türüyle çalışmasına izin verdiğinden, kesişenlerle ortaktır. Örneğin:
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; }
}
özniteliğinin [NotMapped]
bu özelliğin yalnızca varlıkla çalışırken kullanıldığını ve veritabanında kalıcı olmaması gerektiğini belirtmek için kullanıldığına dikkat edin.
Kesme noktası daha sonra uygun yöntemi IMaterializationInterceptor
uygulamalı ve alınan zamanı ayarlamalıdır:
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
bu kesme noktasının bir örneği, yapılandırılırken DbContext
kaydedilir:
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");
}
İpucu
Bu kesme noktası durum bilgisi olmadığından ortaktır, bu nedenle tek bir örnek oluşturulur ve tüm DbContext
örnekler arasında paylaşılır.
Artık veritabanından bir Customer
sorgulandığında özelliği Retrieved
otomatik olarak ayarlanır. Örneğin:
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()}'");
}
Çıktı oluşturur:
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
Varlıklara hizmet ekleme
İpucu
Burada gösterilen kod InjectLoggerSample.cs gelir.
EF Core, bağlam örneklerine bazı özel hizmetler eklemek için yerleşik desteğe zaten sahiptir; örneğin, hizmeti ekleyerek ILazyLoader
çalışan proxy'ler olmadan yavaş yükleme bölümüne bakın.
IMaterializationInterceptor
bunu herhangi bir hizmette genelleştirmek için kullanılabilir. Aşağıdaki örnek, varlıkların kendi günlüklerini gerçekleştirebilecekleri şekilde nasıl eklendiğini ILogger gösterir.
Not
Varlıklara hizmet eklemek, bu varlık türlerini eklenen hizmetlerle eşleştirir ve bu da bazı kişilerin deseni önleme olarak kabul eder.
Daha önce olduğu gibi, neler yapılabileceğini tanımlamak için bir arabirim kullanılır.
public interface IHasLogger
{
ILogger? Logger { get; set; }
}
Ve günlüğe kaydedecek varlık türlerinin bu arabirimi uygulaması gerekir. Örneğin:
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; }
}
Bu kez, kesme noktasının her varlık örneği oluşturulduktan ve özellik değerleri başlatıldıktan sonra çağrılan uygulamasını IMaterializationInterceptor.InitializedInstance
gerekir. Kesme noktası, bağlamdan bir ILogger
alır ve onunla başlatır IHasLogger.Logger
:
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;
}
}
Bu kez her örnek için DbContext
yeni bir kesme noktası örneği kullanılır, çünkü ILogger
elde edilen örnek her örnek için DbContext
değişebilir ve ILogger
kesme noktası üzerinde önbelleğe alınır:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
Şimdi, Customer.PhoneNumber
her değiştirildiğinde, bu değişiklik uygulamanın günlüğüne kaydedilir. Örneğin:
info: CustomersLogger[1]
Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.
LINQ ifade ağacı kesme noktası
İpucu
Burada gösterilen kod QueryInterceptionSample.cs gelir.
EF Core, .NET LINQ sorgularını kullanır. Bu genellikle EF Core tarafından uygun SQL'e çevrilen bir ifade ağacı oluşturmak için C#, VB veya F# derleyicisini kullanmayı içerir. Örneğin, bir müşteri sayfası döndüren bir yöntem düşünün:
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();
}
İpucu
Bu sorgu, EF.Property sıralama ölçütü olarak özelliğini belirtmek için yöntemini kullanır. Bu, uygulamanın özellik adını dinamik olarak geçirmesini sağlar ve varlık türünün herhangi bir özelliğine göre sıralamaya izin verir. Dizine alınamayan sütunlara göre sıralamanın yavaş olabileceğini unutmayın.
Sıralama için kullanılan özellik her zaman kararlı bir sıralama döndürdüğü sürece bu işlem düzgün çalışır. Ama her zaman böyle olmayabilir. Örneğin, yukarıdaki LINQ sorgusu tarafından sıralandığında Customer.City
SQLite'te aşağıdakileri oluşturur:
SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0
aynı City
olan birden çok müşteri varsa, bu sorgunun sıralaması kararlı değildir. Bu, kullanıcı veriler arasında sayfa oluştururken eksik veya yinelenen sonuçlara yol açabilir.
Bu sorunu düzeltmenin yaygın bir yolu, birincil anahtara göre ikincil bir sıralama gerçekleştirmektir. Ancak EF7, bunu her sorguya el ile eklemek yerine, ikincil sıralamanın dinamik olarak eklenebildiği sorgu ifade ağacının kesilmesine izin verir. Bunu kolaylaştırmak için, bu kez tamsayı birincil anahtarı olan herhangi bir varlık için bir arabirim kullanacağız:
public interface IHasIntKey
{
int Id { get; }
}
Bu arabirim, ilgilendiğim varlık türleri tarafından uygulanır:
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; }
}
Ardından uygulayan bir kesiciye ihtiyacımız var 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);
}
}
}
Bu muhtemelen oldukça karmaşık görünüyor ve öyle! İfade ağaçlarıyla çalışmak genellikle kolay değildir. Şimdi neler olduğuna bakalım:
Temel olarak, kesme noktası bir ExpressionVisitorkapsüller. Ziyaretçi, sorgu ifade ağacında bir yönteme çağrı olduğunda çağrılan öğesini geçersiz kılar VisitMethodCall.
Ziyaretçi, bunun ilgilendiğimiz yönteme OrderBy yapılan bir çağrı olup olmadığını denetler.
Bu durumda, ziyaretçi genel yöntem çağrısının arabirimimizi
IHasIntKey
uygulayan bir tür olup olmadığını daha fazla denetler.Bu noktada yöntem çağrısının biçiminde
OrderBy(e => ...)
olduğunu biliyoruz. Bu çağrıdan lambda ifadesini ayıklar ve bu ifadede kullanılan parametresini (yani ,e
) alırız.Şimdi oluşturucu yöntemini kullanarak Expression.Call yeni MethodCallExpression bir derleme yapıyoruz. Bu durumda, çağrılan yöntem şeklindedir
ThenBy(e => e.Id)
. Bunu, yukarıda ayıklanan parametresini ve arabirimininId
IHasIntKey
özelliğine bir özellik erişimi kullanarak oluştururuz.Bu çağrının girişi özgün
OrderBy(e => ...)
öğesidir ve sonuç içinOrderBy(e => ...).ThenBy(e => e.Id)
bir ifadedir.Bu değiştirilen ifade ziyaretçiden döndürülür; başka bir deyişle LINQ sorgusu artık bir
ThenBy
çağrı içerecek şekilde uygun şekilde değiştirilmiştir.EF Core devam eder ve bu sorgu ifadesini kullanılan veritabanı için uygun SQL'de derler.
Bu kesme noktası, ilk örnektekiyle aynı şekilde kaydedilir. Yürütme GetPageOfCustomers
işlemi şimdi aşağıdaki SQL'i oluşturur:
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
Bu, aynı City
olan birden çok müşteri olsa bile artık her zaman kararlı bir sıralama üretecektir.
Vay! Bu, sorguda basit bir değişiklik yapmak için çok fazla kod sağlar. Daha da kötüsü, tüm sorgular için çalışmayabilir. Olması gereken tüm sorgu şekillerini tanıyan ve olmaması gerekenlerin hiçbirini tanımayan bir ifade ziyaretçisi yazmak oldukça zordur. Örneğin, sıralama bir alt sorguda yapılırsa bu büyük olasılıkla işe yaramaz.
Bu, bizi kesme noktalarıyla ilgili kritik bir noktaya getirir; her zaman kendinize istediğinizi yapmanın daha kolay bir yolu olup olmadığını sorun. Kesiciler güçlü olsa da, işleri yanlış yapmak kolaydır. Onlar, dedikleri gibi, kendini ayağından vurmanın kolay bir yolu.
Örneğin, bunun yerine yöntemimizi GetPageOfCustomers
şu şekilde değiştirip değiştirmediğimize bakın:
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();
}
Bu durumda, ThenBy
basitçe sorguya eklenir. Evet, her sorgu için ayrı ayrı yapılması gerekebilir, ancak basit, anlaşılması kolaydır ve her zaman çalışır.
İyimser eşzamanlılık kesme
İpucu
Burada gösterilen kod OptimisticConcurrencyInterceptionSample.cs gelir.
EF Core, bir güncelleştirmeden veya silmeden gerçekten etkilenen satır sayısının, etkilenmesi beklenen satır sayısıyla aynı olup olmadığını denetleyerek iyimser eşzamanlılık düzenini destekler. Bu genellikle eşzamanlılık belirteci ile birlikte kullanılır; başka bir ifadeyle, yalnızca beklenen değer okunduktan sonra satır güncelleştirilmediyse beklenen değeriyle eşleşecek bir sütun değeridir.
EF, bir DbUpdateConcurrencyExceptionoluşturarak iyimser eşzamanlılık ihlaline işaret eder. EF7'de, ISaveChangesInterceptor yeni yöntemleri ThrowingConcurrencyException
vardır ve ThrowingConcurrencyExceptionAsync
bu yöntemler atmadan önce DbUpdateConcurrencyException
çağrılır. Bu kesme noktaları, ihlali çözmek için büyük olasılıkla zaman uyumsuz veritabanı değişiklikleriyle birlikte özel durumun gizlenmesine olanak sağlar.
Örneğin, iki istek aynı varlığı hemen hemen aynı anda silmeyi denerse, veritabanındaki satır artık mevcut olmadığından ikinci silme işlemi başarısız olabilir. Bu sorun olmayabilir; sonuçta varlığın yine de silinmiş olması gerekir. Aşağıdaki kesme noktası bunun nasıl yapılabilmesini gösterir:
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));
}
Bu kesme noktası hakkında dikkate değer birkaç şey vardır:
- Hem zaman uyumlu hem de zaman uyumsuz kesme yöntemleri uygulanır. Uygulama veya
SaveChangesAsync
çağrısıSaveChanges
yapabilirse bu önemlidir. Ancak, tüm uygulama kodu zaman uyumsuzsa, yalnızcaThrowingConcurrencyExceptionAsync
uygulanması gerekir. Benzer şekilde, uygulama hiçbir zaman zaman uyumsuz veritabanı yöntemlerini kullanmıyorsa, yalnızcaThrowingConcurrencyException
uygulanması gerekir. Bu genellikle eşitleme ve zaman uyumsuz yöntemlere sahip tüm kesme makineleri için geçerlidir. (Bazı eşitleme/zaman uyumsuz kodların ürpermesi durumunda uygulamanızın oluşturmak için kullanmadığı yöntemi uygulamak faydalı olabilir.) - Kesme noktasının, kaydedilen varlıklar için nesnelere EntityEntry erişimi vardır. Bu durumda, silme işlemi için eşzamanlılık ihlali olup olmadığını denetlemek için kullanılır.
- Uygulama ilişkisel veritabanı sağlayıcısı kullanıyorsa, ConcurrencyExceptionEventData nesne bir RelationalConcurrencyExceptionEventData nesneye yayınlanabilir. Bu, gerçekleştirilen veritabanı işlemi hakkında ilişkisel olarak ek bilgiler sağlar. Bu durumda, ilişkisel komut metni konsola yazdırılır.
- Döndüren
InterceptionResult.Suppress()
, EF Core'a yapmak üzere olduğu eylemi gizlemesini bildirir; bu durumda,DbUpdateConcurrencyException
oluşturur. EF Core'un davranışını değiştirmek, EF Core'un ne yaptığını gözlemlemek yerine, kesme noktası avcılarının en güçlü özelliklerinden biridir.
bağlantı dizesi yavaş başlatma
İpucu
Burada gösterilen kod LazyConnectionStringSample.cs gelir.
Bağlantı dizeleri genellikle yapılandırma dosyasından okunan statik varlıklardır. Bunlar, yapılandırırken kolayca veya UseSqlServer
benzer bir DbContext
şekilde geçirilebilir. Ancak, bazen bağlantı dizesi her bağlam örneği için değişebilir. Örneğin, çok kiracılı bir sistemdeki her kiracının farklı bir bağlantı dizesi olabilir.
EF7, geliştirmeleri aracılığıyla dinamik bağlantıları ve bağlantı dizesi işlemeyi IDbConnectionInterceptorkolaylaştırır. Bu, herhangi bir bağlantı dizesi olmadan öğesini DbContext
yapılandırma özelliğiyle başlar. Örneğin:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer());
Bu yöntemlerden IDbConnectionInterceptor
biri, kullanılmadan önce bağlantıyı yapılandırmak için uygulanabilir. ConnectionOpeningAsync
bağlantı dizesi almak, erişim belirteci bulmak vb. için zaman uyumsuz bir işlem gerçekleştirebildiğinden iyi bir seçimdir. Örneğin, kapsamı geçerli kiracıyı anlayan geçerli istek kapsamında bir hizmet düşünün:
services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();
Uyarı
Bir bağlantı dizesi, erişim belirteci veya benzer bir zaman uyumsuz aramanın her gerektiğinde gerçekleştirilmesi çok yavaş olabilir. Bu öğeleri önbelleğe almayı ve yalnızca önbelleğe alınan dizeyi veya belirteci düzenli aralıklarla yenilemeyi göz önünde bulundurun. Örneğin, erişim belirteçleri genellikle yenilenmesi gerekmeden önce önemli bir süre kullanılabilir.
Bu, oluşturucu eklemesi kullanılarak her DbContext
örneğe eklenebilir:
public class CustomerContext : DbContext
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public CustomerContext(
DbContextOptions<CustomerContext> options,
ITenantConnectionStringFactory connectionStringFactory)
: base(options)
{
_connectionStringFactory = connectionStringFactory;
}
// ...
}
Bu hizmet daha sonra bağlam için kesme noktası uygulaması oluşturulurken kullanılır:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
Son olarak, kesme noktası bağlantı dizesi zaman uyumsuz olarak almak ve bağlantı ilk kez kullanıldığında ayarlamak için bu hizmeti kullanır:
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;
}
}
Not
bağlantı dizesi yalnızca ilk kez bağlantı kullanıldığında elde edilir. Bundan sonra, üzerinde DbConnection
depolanan bağlantı dizesi yeni bir bağlantı dizesi aramadan kullanılır.
İpucu
Bu kesme noktası, bağlantı dizesi almak için hizmetin zaman uyumsuz bir kod yolundan çağrılması gerektiğinden, oluşturulacak zaman ConnectionOpening
uyumsuz yöntemi geçersiz kılar.
SQL Server sorgu istatistiklerini günlüğe kaydetme
İpucu
Burada gösterilen kod QueryStatisticsLoggerSample.cs gelir.
Son olarak, sql server sorgu istatistiklerini uygulama günlüğüne göndermek için birlikte çalışan iki kesme noktası oluşturalım. İstatistikleri oluşturmak için iki şey yapmamız gerekir IDbCommandInterceptor .
İlk olarak, kesme noktası komutları ile SET STATISTICS IO ON
ön ekleyecek ve sql server'a bir sonuç kümesi tüketildikten sonra istemciye istatistik göndermesini söyler:
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);
}
İkinci olarak, kesme noktası, sonuçların tüketilmesi tamamlandıktan sonra DbDataReader ancak kapatılmadan önce çağrılan yeni DataReaderClosingAsync
yöntemi uygular. SQL Server istatistik gönderirken, bunları okuyucuda ikinci bir sonuca yerleştirir, bu nedenle bu noktada kesme noktası, bağlantıda istatistikleri dolduran çağrı NextResultAsync
yaparak sonucu okur.
public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
DbCommand command,
DataReaderClosingEventData eventData,
InterceptionResult result)
{
await eventData.DataReader.NextResultAsync();
return result;
}
İkinci kesme noktası, bağlantıdan istatistikleri almak ve bunları uygulamanın günlükçüsine yazmak için gereklidir. Bunun için yeni ConnectionCreated
yöntemi uygulayan bir IDbConnectionInterceptorkullanacağız. ConnectionCreated
, EF Core bir bağlantı oluşturduktan hemen sonra çağrılır ve bu nedenle bu bağlantının ek yapılandırmasını gerçekleştirmek için kullanılabilir. Bu durumda, kesme noktası bir ILogger
alır ve sonra iletileri günlüğe kaydetmek için olaya takılır SqlConnection.InfoMessage .
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;
}
Önemli
ConnectionCreating
ve ConnectionCreated
yöntemleri yalnızca EF Core bir DbConnection
oluşturduğunda çağrılır. Uygulama oluşturur DbConnection
ve EF Core'a geçirirse çağrılmaz.
Bu kesicileri kullanan bazı kodlar çalıştırılırken günlükte SQL Server sorgu istatistikleri gösterilir:
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.
Sorgu geliştirmeleri
EF7, LINQ sorgularının çevirisinde birçok geliştirme içerir.
Son işleç olarak GroupBy
İpucu
Burada gösterilen kod GroupByFinalOperatorSample.cs gelir.
EF7, sorguda son işleç olarak kullanmayı GroupBy
destekler. Örneğin, bu LINQ sorgusu:
var query = context.Books.GroupBy(s => s.Price);
SQL Server kullanırken aşağıdaki SQL'e çevrilir:
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]
Not
Bu tür GroupBy
doğrudan SQL'e çevrilmediğinden EF Core, döndürülen sonuçlarda gruplandırma yapar. Ancak bu, sunucudan herhangi bir ek veri aktarılmasına neden olmaz.
Son işleç olarak GroupJoin
İpucu
Burada gösterilen kod GroupJoinFinalOperatorSample.cs gelir.
EF7, sorguda son işleç olarak kullanmayı GroupJoin
destekler. Örneğin, bu LINQ sorgusu:
var query = context.Customers.GroupJoin(
context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });
SQL Server kullanırken aşağıdaki SQL'e çevrilir:
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]
GroupBy varlık türü
İpucu
Burada gösterilen kod GroupByEntityTypeSample.cs gelir.
EF7, varlık türüne göre gruplandırma işlemini destekler. Örneğin, bu LINQ sorgusu:
var query = context.Books
.GroupBy(s => s.Author)
.Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });
SQLite kullanılırken aşağıdaki SQL'e çevrilir:
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]
Birincil anahtar gibi benzersiz bir özelliğe göre gruplandırma işleminin her zaman varlık türüne göre gruplandırmaktan daha verimli olacağını unutmayın. Ancak, varlık türlerine göre gruplandırma hem anahtarlı hem de anahtarsız varlık türleri için kullanılabilir.
Ayrıca, her varlığın benzersiz bir anahtar değerine sahip olması gerektiğinden, birincil anahtara sahip bir varlık türüne göre gruplandırma her zaman varlık örneği başına bir grupla sonuçlanır. Bazen içinde gruplandırma gerekli olmayacak şekilde sorgunun kaynağını değiştirmeye değer. Örneğin, aşağıdaki sorgu önceki sorguyla aynı sonuçları döndürür:
var query = context.Authors
.Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });
Bu sorgu, SQLite kullanılırken aşağıdaki SQL'e çevrilir:
SELECT [a].[Id], [a].[Name], (
SELECT MAX([b].[Price])
FROM [Books] AS [b]
WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]
Alt sorgular dış sorgudaki gruplandırılmamış sütunlara başvuramaz
İpucu
Burada gösterilen kod UngroupedColumnsQuerySample.cs gelir.
EF Core 6.0'da yan GROUP BY
tümcesi dış sorgudaki sütunlara başvurarak bazı veritabanlarında başarısız olur ve bazılarında verimsiz olur. Örneğin, aşağıdaki sorguyu göz önüne alın:
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)
};
SQL Server'da EF Core 6.0'da bu, şu şekilde çevrildi:
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])
EF7'de çeviri şu şekildedir:
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]
Salt okunur koleksiyonlar için kullanılabilir Contains
İpucu
Burada gösterilen kod ReadOnlySetQuerySample.cs gelir.
EF7, aranacak öğeler veya IReadOnlySet
veya IReadOnlyCollection
IReadOnlyList
içinde bulunduğunda kullanmayı Contains
destekler. Örneğin, bu LINQ sorgusu:
IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));
SQL Server kullanırken aşağıdaki SQL'e çevrilir:
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))
Toplama işlevleri için çeviriler
EF7, sağlayıcıların toplama işlevlerini çevirmesi için daha iyi genişletilebilirlik sağlar. Bu ve bu alandaki diğer çalışmalar, sağlayıcılar arasında aşağıdakiler de dahil olmak üzere birkaç yeni çeviriye neden oldu:
- ve çevirisi
String.Join
String.Concat
- Uzamsal toplama işlevlerinin çevirisi
- İstatistik toplama işlevlerinin çevirisi
Not
Bağımsız değişken üzerinde IEnumerable
işlem gören toplama işlevleri genellikle yalnızca sorgularda GroupBy
çevrilir. Bu sınırlamanın kaldırılmasını istiyorsanız JSON sütunlarında Uzamsal türleri destekleme için oy verin.
Dize toplama işlevleri
İpucu
Burada gösterilen kod StringAggregateFunctionsSample.cs gelir.
ve Concat kullanan Join sorgular artık uygun olduğunda çevriliyor. Örneğin:
var query = context.Posts
.GroupBy(post => post.Author)
.Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });
Bu sorgu, SQL Server kullanılırken aşağıdakine çevrilir:
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]
Diğer dize işlevleriyle birleştirildiğinde, bu çeviriler sunucuda bazı karmaşık dize düzenlemelerine olanak tanır. Örneğin:
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) + "' "))
});
Bu sorgu, SQL Server kullanılırken aşağıdakine çevrilir:
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]
Uzamsal toplama işlevleri
İpucu
Burada gösterilen kod SpatialAggregateFunctionsSample.cs gelir.
Artık NetTopologySuite'ı destekleyen veritabanı sağlayıcılarının aşağıdaki uzamsal toplama işlevlerini çevirmesi mümkündür:
- GeometryCombiner.Combine()
- UnaryUnionOp.Union()
- ConvexHull.Create()
- EnvelopeCombiner.CombineAsGeometry()
İpucu
Bu çeviriler SQL Server ve SQLite için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.
Örneğin:
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)) });
Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:
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]
İstatistiksel toplama işlevleri
İpucu
Burada gösterilen kod StatisticalAggregateFunctionsSample.cs gelir.
SQL Server çevirileri aşağıdaki istatistiksel işlevler için uygulanmıştır:
İpucu
Bu çeviriler SQL Server için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.
Örneğin:
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))
});
Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:
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]
Çevirisi string.IndexOf
İpucu
Burada gösterilen kod MiscellaneousTranslationsSample.cs gelir.
EF7 artık LINQ sorgularında çevrilmektedir String.IndexOf . Örneğin:
var query = context.Posts
.Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
.Where(post => post.IndexOfEntity > 0);
Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:
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
Varlık türleri için çevirisi GetType
İpucu
Burada gösterilen kod MiscellaneousTranslationsSample.cs gelir.
EF7 artık LINQ sorgularında çevrilmektedir Object.GetType() . Örneğin:
var query = context.Posts.Where(post => post.GetType() == typeof(Post));
Bu sorgu, TPH devralma ile SQL Server kullanılırken aşağıdaki SQL'e çevrilir:
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'
Bu sorgunun türetilmiş türlerin değil, yalnızca Post
türünde Post
örnekleri döndürdüğüne dikkat edin. Bu, veya OfType
kullanan is
ve türetilmiş türlerin örneklerini de döndürecek bir sorgudan farklıdır. Örneğin, şu sorguyu göz önünde bulundurun:
var query = context.Posts.OfType<Post>();
Bu, farklı SQL'e çevrilir:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
Ve hem hem FeaturedPost
de Post
varlıkları döndürür.
Için destek AT TIME ZONE
İpucu
Burada gösterilen kod MiscellaneousTranslationsSample.cs gelir.
EF7, ve DateTimeOffsetiçin DateTime yeni AtTimeZone işlevler sağlar. Bu işlevler, oluşturulan SQL'deki yan tümcelere çevrilir AT TIME ZONE
. Örneğin:
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"),
});
Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:
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]
İpucu
Bu çeviriler SQL Server için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.
Filtrelenmiş Gizli gezintilere ekle
İpucu
Burada gösterilen kod MiscellaneousTranslationsSample.cs gelir.
Include yöntemleri artık ile EF.Propertykullanılabilir. Bu, özel gezinti özellikleri veya alanlar tarafından temsil edilen özel gezintiler için bile filtrelemeye ve sıralamaya olanak tanır. Örneğin:
var query = context.Blogs.Include(
blog => EF.Property<ICollection<Post>>(blog, "Posts")
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Bu, şuna eşdeğerdir:
var query = context.Blogs.Include(
blog => Posts
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Ancak genel olarak erişilebilir olmasını gerektirmez Blog.Posts
.
SQL Server kullanırken, yukarıdaki her iki sorgu da şu şekilde çevrilir:
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]
için Cosmos çevirisi Regex.IsMatch
İpucu
Burada gösterilen kod CosmosQueriesSample.cs gelir.
EF7, Azure Cosmos DB'ye yönelik LINQ sorgularında kullanmayı Regex.IsMatch destekler. Örneğin:
var containsInnerT = await context.Triangles
.Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
.ToListAsync();
Aşağıdaki SQL'e çevrilir:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))
DbContext API ve davranış geliştirmeleri
EF7, ve ilgili sınıflar için DbContext çeşitli küçük iyileştirmeler içerir.
İpucu
Bu bölümdeki örneklerin kodu DbContextApiSample.cs gelir.
Başlatılmamış DbSet özellikleri için suppressor
üzerinde genel, ayarlanabilir DbSet
özellikler DbContext
, oluşturulurken DbContext
EF Core tarafından otomatik olarak başlatılır. Örneğin, aşağıdaki DbContext
tanımı göz önünde bulundurun:
public class SomeDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}
özelliği, Blogs
örneği oluşturmanın DbContext
bir parçası olarak bir DbSet<Blog>
örneğe ayarlanır. Bu, bağlamın ek adımlar olmadan sorgular için kullanılmasını sağlar.
Ancak, C# null atanabilir başvuru türlerinin kullanıma sunulmasının ardından, derleyici artık null atanamayan özelliğin Blogs
başlatılmadığı konusunda uyarır:
[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Bu sahte bir uyarıdır; özelliği EF Core tarafından null olmayan bir değere ayarlanır. Ayrıca, özelliğin null atanabilir olarak bildirilmesi uyarının gitmesini sağlar, ancak kavramsal olarak özellik null atanamaz ve hiçbir zaman null olmadığından bu iyi bir fikir değildir.
EF7, derleyicinin bu uyarıyı oluşturmasını durduran bir DbContext
üzerindeki özellikler için DbSet
DiagnosticSuppressor içerir.
İpucu
Bu desen, C# otomatik özelliklerinin çok sınırlı olduğu günlerde ortaya çıkmıştır. Modern C# ile otomatik özellikleri salt okunur yapmayı ve sonra bunları oluşturucuda açıkça başlatmayı DbContext
veya gerektiğinde önbelleğe alınmış DbSet
örneği bağlamdan almayı göz önünde bulundurun. Örneğin, public DbSet<Blog> Blogs => Set<Blog>()
.
İptali günlüklerdeki hatadan ayırt etme
Bazen bir uygulama bir sorguyu veya başka bir veritabanı işlemini açıkça iptal eder. Bu genellikle işlemi gerçekleştiren yöntemine geçirilen bir CancellationToken kullanılarak yapılır.
EF Core 6'da, bir işlem iptal edildiğinde günlüğe kaydedilen olaylar, işlem başka bir nedenle başarısız olduğunda günlüğe kaydedilen olaylarla aynıdır. EF7, iptal edilen veritabanı işlemleri için özel olarak yeni günlük olaylarını tanıtır. Bu yeni olaylar varsayılan olarak düzeyinde günlüğe Debug kaydedilir. Aşağıdaki tabloda ilgili olaylar ve bunların varsayılan günlük düzeyleri gösterilmektedir:
Olay | Açıklama | Varsayılan günlük düzeyi |
---|---|---|
CoreEventId.QueryIterationFailed | Sorgu sonuçları işlenirken bir hata oluştu. | LogLevel.Error |
CoreEventId.SaveChangesFailed | Değişiklikleri veritabanına kaydetmeye çalışırken bir hata oluştu. | LogLevel.Error |
RelationalEventId.CommandError | Veritabanı komutu yürütülürken bir hata oluştu. | LogLevel.Error |
CoreEventId.QueryCanceled | Sorgu iptal edildi. | LogLevel.Debug |
CoreEventId.SaveChangesCanceled | Değişiklikleri kaydetmeye çalışılırken veritabanı komutu iptal edildi. | LogLevel.Debug |
RelationalEventId.CommandCanceled | yürütmesi DbCommand iptal edildi. |
LogLevel.Debug |
Not
İptal belirtecini denetlemek yerine özel duruma bakılarak iptal algılanır. Bu, iptal belirteci aracılığıyla tetiklenmeyen iptallerin yine de algılanacağı ve bu şekilde günlüğe kaydedileceği anlamına gelir.
Yöntemler için EntityEntry
yeni IProperty
ve INavigation
aşırı yüklemeler
EF modeliyle çalışan kodun genellikle bir IProperty veya INavigation temsil eden özelliği veya gezinti meta verileri olur. Ardından bir EntityEntry özelliği/gezinti değerini almak veya durumunu sorgulamak için kullanılır. Ancak, EF7'nin öncesinde, bu işlem özelliğin adını veya gezintiyi yöntemine EntityEntry
geçirerek veya INavigation
öğesini yeniden ararIProperty
. EF7'de, IProperty
ek aramadan kaçınarak veya INavigation
doğrudan geçirilebilir.
Örneğin, belirli bir varlığın tüm eşdüzeylerini bulmak için bir yöntem düşünün:
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));
}
Bu yöntem, belirli bir varlığın üst öğesini bulur ve ardından üst girişin Collection
yöntemine tersini INavigation
geçirir. Bu meta veriler daha sonra verilen üst öğesinin tüm eşdüzeylerini döndürmek için kullanılır. Kullanım örneği aşağıda verilmiştir:
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}'");
}
Ve çıkış:
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
paylaşılan tür varlık türleri için
EF Core, birden çok farklı varlık türü için aynı CLR türünü kullanabilir. Bunlar "paylaşılan tür varlık türleri" olarak bilinir ve genellikle bir sözlük türünü varlık türünün özellikleri için kullanılan anahtar/değer çiftleriyle eşlemek için kullanılır. Örneğin, bir BuildMetadata
varlık türü ayrılmış bir CLR türü tanımlamadan tanımlanabilir:
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");
});
Paylaşılan tür varlık türünün adlandırılması gerektiğine dikkat edin. Bu durumda, adı şeklindedir BuildMetadata
. Bu varlık türlerine daha sonra ad kullanılarak elde edilen varlık türü için bir DbSet
kullanılarak erişilir. Örneğin:
public DbSet<Dictionary<string, object>> BuildMetadata
=> Set<Dictionary<string, object>>("BuildMetadata");
Bu DbSet
, varlık örneklerini izlemek için kullanılabilir:
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" }
});
Ve sorguları yürüt:
var builds = await context.BuildMetadata
.Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
.OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
.ToListAsync();
Şimdi EF7'de, henüz izlenmese bile bir örneğin durumunu elde etmek için kullanılabilecek bir yöntem DbSet
de Entry
vardır. Örneğin:
var state = context.BuildMetadata.Entry(build).State;
ContextInitialized
şu anda şu şekilde günlüğe kaydedilir: Debug
EF7'de olay ContextInitialized düzeyinde günlüğe Debug kaydedilir. Örneğin:
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
Önceki sürümlerde, düzeyinde günlüğe Information kaydedildi. Örneğin:
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
İsterseniz günlük düzeyi olarak Information
değiştirilebilir:
optionsBuilder.ConfigureWarnings(
builder =>
{
builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
});
IEntityEntryGraphIterator
genel kullanıma açık
EF7'de hizmet IEntityEntryGraphIterator uygulamalar tarafından kullanılabilir. Bu, hem izlenip hem de tarafından TrackGraphizlenen varlıkların grafiğini keşfederken dahili olarak kullanılan hizmettir. Aşağıda, bir başlangıç varlığından ulaşılabilen tüm varlıklar üzerinde yinelemeye yönelik bir örnek verilmiştir:
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();
Not:
- Geri çağırma temsilcisi döndürdüğünde
false
yineleyici belirli bir düğümden geçişi durdurur. Bu örnek, ziyaret edilen varlıkları izler ve varlık zaten ziyaret edildiğinde döndürürfalse
. Bu, grafikteki döngülerden kaynaklanan sonsuz döngüleri önler. - nesnesi,
EntityEntryGraphNode<TState>
durumun temsilciye yakalanmadan geçirilmesini sağlar. - İlk düğüm dışında ziyaret edilen her düğüm için, bulunduğu düğüm ve aracılığıyla bulunduğu gezinti geri çağırmaya geçirilir.
Model oluşturma geliştirmeleri
EF7, model oluşturmada çeşitli küçük geliştirmeler içerir.
İpucu
Bu bölümdeki örneklerin kodu ModelBuildingSample.cs gelir.
Dizinler artan veya azalan olabilir
Varsayılan olarak EF Core artan dizinler oluşturur. EF7, azalan dizinlerin oluşturulmasını da destekler. Örneğin:
modelBuilder
.Entity<Post>()
.HasIndex(post => post.Title)
.IsDescending();
Veya eşleme özniteliğini Index
kullanarak:
[Index(nameof(Title), AllDescending = true)]
public class Post
{
public int Id { get; set; }
[MaxLength(64)]
public string? Title { get; set; }
}
Veritabanı her iki yönde de sıralama için aynı dizini kullanabileceğinden, bu özellik tek bir sütundaki dizinler için nadiren yararlıdır. Ancak, her sütundaki sıranın önemli olabileceği birden çok sütundaki bileşik dizinler için bu durum geçerli değildir. EF Core, birden çok sütunun her sütun için tanımlanmış farklı sıralamaya sahip olmasını sağlayarak bunu destekler. Örneğin:
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner })
.IsDescending(false, true);
Veya eşleme özniteliğini kullanarak:
[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();
}
Bu, SQL Server kullanılırken aşağıdaki SQL'e neden olur:
CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);
Son olarak, dizin adları verilerek aynı sıralı sütun kümesi üzerinde birden çok dizin oluşturulabilir. Örneğin:
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);
Veya eşleme özniteliklerini kullanarak:
[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();
}
Bu, SQL Server'da aşağıdaki SQL'i oluşturur:
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);
Bileşik anahtarlar için eşleme özniteliği
EF7, herhangi bir varlık türünün birincil anahtar özelliğini veya özelliklerini belirtmek için yeni bir eşleme özniteliği ("veri ek açıklaması") sunar. aksine System.ComponentModel.DataAnnotations.KeyAttribute, PrimaryKeyAttribute anahtar özelliği yerine varlık türü sınıfına yerleştirilir. Örneğin:
[PrimaryKey(nameof(PostKey))]
public class Post
{
public int PostKey { get; set; }
}
Bu, bileşik anahtarları tanımlamak için doğal bir uyum sağlar:
[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
public int PostId { get; set; }
public int CommentId { get; set; }
public string CommentText { get; set; } = null!;
}
Sınıfında dizini tanımlamak, özel özellikleri veya alanları anahtar olarak belirtmek için de kullanılabileceğini, ancak EF modelini oluştururken genellikle yoksayılacağını gösterir. Örneğin:
[PrimaryKey(nameof(_id))]
public class Tag
{
private readonly int _id;
}
DeleteBehavior
eşleme özniteliği
EF7, bir ilişkinin belirtilmesi DeleteBehavior için bir eşleme özniteliği ("veri ek açıklaması") sunar. Örneğin, gerekli ilişkiler varsayılan olarak ile DeleteBehavior.Cascade oluşturulur. Bu, kullanılarak DeleteBehaviorAttributevarsayılan olarak olarak DeleteBehavior.NoAction değiştirilebilir:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
[DeleteBehavior(DeleteBehavior.NoAction)]
public Blog Blog { get; set; } = null!;
}
Bu, Blog Gönderileri ilişkisi için art arda silmeleri devre dışı bırakır.
Farklı sütun adlarına eşlenen özellikler
Bazı eşleme desenleri, aynı CLR özelliğinin birden çok farklı tablonun her birindeki bir sütuna eşlenmesine neden olur. EF7, bu sütunların farklı adlara sahip olmasını sağlar. Örneğin, basit bir devralma hiyerarşisini göz önünde bulundurun:
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; }
}
TPT devralma eşleme stratejisiyle bu türler üç tabloya eşlenir. Ancak, her tablodaki birincil anahtar sütunu farklı bir ada sahip olabilir. Örneğin:
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, bu eşlemenin iç içe tablo oluşturucusu kullanılarak yapılandırılmasına izin verir:
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"));
TPC devralma eşlemesi ile Breed
özelliği, farklı tablolardaki farklı sütun adlarına da eşlenebilir. Örneğin, aşağıdaki TPC tablolarını göz önünde bulundurun:
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 şu tablo eşlemesini destekler:
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");
});
Tek yönlü çoka çok ilişkileri
EF7, bir tarafın veya diğerinin gezinti özelliğine sahip olmadığı çoka çok ilişkileri destekler. Örneğin, ve Tag
türlerini göz önünde bulundurunPost
:
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!;
}
Türün Post
etiket listesi için gezinti özelliği olduğuna, ancak türün Tag
gönderiler için gezinti özelliğine sahip olduğuna dikkat edin. EF7'de bu, çoka çok ilişki olarak yapılandırılabilir ve aynı Tag
nesnenin birçok farklı gönderi için kullanılmasına olanak sağlar. Örneğin:
modelBuilder
.Entity<Post>()
.HasMany(post => post.Tags)
.WithMany();
Bu, uygun birleştirme tablosuna eşlemeyle sonuç olur:
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
);
Ve ilişki normal şekilde çoka çok olarak kullanılabilir. Örneğin, ortak bir kümeden çeşitli etiketleri paylaşan bazı gönderiler ekleme:
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();
Varlık bölme
Varlık bölme tek bir varlık türünü birden çok tabloya eşler. Örneğin, müşteri verilerini barındıran üç tablo içeren bir veritabanı düşünün:
Customers
Müşteri bilgileri tablosuPhoneNumbers
Müşterinin telefon numarası için bir tabloAddresses
Müşterinin adresi için bir tablo
SQL Server'da bu tabloların tanımları şunlardır:
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
);
Bu tabloların her biri genellikle türler arasındaki ilişkilerle kendi varlık türlerine eşlenir. Ancak, üç tablonun hepsi her zaman birlikte kullanılıyorsa, bunların tümünü tek bir varlık türüne eşlemek daha kullanışlı olabilir. Örneğin:
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; }
}
Bu, EF7'de varlık türündeki her bölme için çağrılarak SplitToTable
güncelleştirilir. Örneğin, aşağıdaki kod varlık türünü Customers
yukarıda gösterilen , PhoneNumbers
ve Addresses
tablolarına bölerCustomer
:
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);
});
});
Ayrıca, gerekirse tabloların her biri için farklı birincil anahtar sütun adlarının belirtilebildiğine de dikkat edin.
SQL Server UTF-8 dizeleri
ve nvarchar
veri türleri tarafından nchar
temsil edilen SQL Server Unicode dizeleri UTF-16 olarak depolanır. Ayrıca ve varchar
veri türleri,char
çeşitli karakter kümelerini destekleyen Unicode olmayan dizeleri depolamak için kullanılır.
SQL Server 2019'dan başlayarak ve varchar
veri türleri bunun char
yerine UTF-8 kodlamalı Unicode dizeleri depolamak için kullanılabilir. , UTF-8 harmanlamalarından biri ayarlanarak elde edilir. Örneğin, aşağıdaki kod sütun için değişken uzunlukta bir SQL Server UTF-8 dizesi yapılandırıyor CommentText
:
modelBuilder
.Entity<Comment>()
.Property(comment => comment.CommentText)
.HasColumnType("varchar(max)")
.UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");
Bu yapılandırma aşağıdaki SQL Server sütun tanımını oluşturur:
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])
);
Zamana bağlı tablolar sahip olunan varlıkları destekler
EF Core SQL Server zamana bağlı tablo eşlemesi, EF7'de tablo paylaşımını destekleyecek şekilde geliştirilmiştir. En önemlisi, sahip olunan tek varlıklar için varsayılan eşleme tablo paylaşımını kullanır.
Örneğin, sahip varlık türünü Employee
ve sahip olduğu varlık türünü EmployeeInfo
göz önünde bulundurun:
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; }
}
Bu türler aynı tabloya eşlenmişse EF7'de bu tablo zamana bağlı bir tablo yapılabilir:
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");
}));
Not
Bu yapılandırmayı kolaylaştırma işlemi Sorun #29303 tarafından izlenir. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.
Geliştirilmiş değer oluşturma
EF7, anahtar özellikleri için değerlerin otomatik olarak oluşturulmasında iki önemli geliştirme içerir.
İpucu
Bu bölümdeki örneklerin kodu ValueGenerationSample.cs gelir.
DDD korumalı türler için değer oluşturma
Etki alanı odaklı tasarımda (DDD), "korunan anahtarlar" anahtar özelliklerinin tür güvenliğini iyileştirebilir. Bu, anahtar türü, anahtarın kullanımına özgü başka bir türe kaydırılarak elde edilir. Örneğin, aşağıdaki kod ürün anahtarları için bir ProductId
tür ve kategori anahtarları için bir CategoryId
tür tanımlar.
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; }
}
Bunlar daha sonra ve Category
varlık türlerinde Product
kullanılır:
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();
}
Bu, bir kategorinin kimliğinin yanlışlıkla bir ürüne atanmaması (veya tam tersi) olanaksız hale gelir.
Uyarı
Birçok DDD kavramında olduğu gibi, bu geliştirilmiş tür güvenliği de ek kod karmaşıklığından kaynaklanmıştır. Örneğin, bir kategoriye ürün kimliği atamanın olası bir şey olup olmadığını göz önünde bulundurmaya değer. İşleri basit tutmak, kod tabanı için genel olarak daha yararlı olabilir.
Burada gösterilen korunan anahtar türleri hem anahtar değerlerini sarmalar int
, diğer bir deyişle eşlenen veritabanı tablolarında tamsayı değerleri kullanılır. Bu, türler için değer dönüştürücüleri tanımlanarak elde edilir:
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))
{
}
}
Not
Buradaki kod türleri kullanır struct
. Bu, anahtar olarak kullanmak için uygun değer türü semantiğine sahip oldukları anlamına gelir. Türler bunun yerine kullanılırsa class
, eşitlik semantiğini geçersiz kılmaları veya ayrıca bir değer karşılaştırıcı belirtmeleri gerekir.
EF7'de, değer dönüştürücülerini temel alan anahtar türleri, temel alınan tür bunu desteklediği sürece otomatik olarak oluşturulan anahtar değerlerini kullanabilir. Bu, kullanılarak ValueGeneratedOnAdd
normal şekilde yapılandırılır:
modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();
Varsayılan olarak bu, SQL Server ile kullanıldığında sütunlara IDENTITY
neden olur:
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);
Varlıklar eklenirken anahtar değerleri oluşturmak için normal şekilde kullanılır:
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;
SQL Server için sıra tabanlı anahtar oluşturma
EF Core, SQL Server sütunlarını kullanarak anahtar değeri oluşturmayı veya veritabanı dizisi tarafından oluşturulan anahtar bloklarını temel alan bir Hi-Lo desenini destekler.IDENTITY
EF7, anahtarın sütun varsayılan kısıtlamasına eklenmiş bir veritabanı dizisi için destek sağlar. En basit haliyle, bunun için EF Core'a anahtar özelliği için bir dizi kullanmasını söylemek gerekir:
modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();
Bu, veritabanında bir sıranın tanımlanmasına neden olur:
CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;
Daha sonra anahtar sütunu varsayılan kısıtlamasında kullanılır:
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);
Not
Bu anahtar oluşturma biçimi, TPC eşleme stratejisi kullanılarak varlık türü hiyerarşilerinde oluşturulan anahtarlar için varsayılan olarak kullanılır.
İsterseniz, diziye farklı bir ad ve şema verilebilir. Örneğin:
modelBuilder
.Entity<Product>()
.Property(product => product.Id)
.UseSequence("ProductsSequence", "northwind");
Dizinin daha fazla yapılandırması, modelde açıkça yapılandırılarak oluşturulur. Örneğin:
modelBuilder
.HasSequence<int>("ProductsSequence", "northwind")
.StartsAt(1000)
.IncrementsBy(2);
Geçiş araçları geliştirmeleri
EF7, EF Core Migrations komut satırı araçlarını kullanırken iki önemli geliştirme içerir.
UseSqlServer vb. null kabul et
Yapılandırma dosyasından bir bağlantı dizesi okumak ve ardından bu bağlantı dizesi , UseSqlite
veya başka bir sağlayıcı için eşdeğer yönteme UseSqlServer
geçirmek çok yaygındır. Örneğin:
services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));
Geçişler uygulanırken bağlantı dizesi geçirmek de yaygın bir durumdur. Örneğin:
dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
Veya Migrations paketi kullanırken.
./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
Bu durumda, yapılandırmadan okuma bağlantı dizesi kullanılmasa da, uygulama başlangıç kodu yapılandırmadan okumaya ve öğesine geçirmeye UseSqlServer
çalışır. Yapılandırma kullanılamıyorsa, bu işlem null değerinin öğesine geçirilmesiyle UseSqlServer
sonuçlanır. EF7'de, bağlantı dizesi daha sonra ayarlandığı sürece, örneğin komut satırı aracına geçirilerek --connection
buna izin verilir.
Not
Bu değişiklik ve UseSqlite
için UseSqlServer
yapılmıştır. Diğer sağlayıcılar için, sağlayıcı için henüz yapılmamışsa eşdeğer bir değişiklik yapmak üzere sağlayıcı bakımcısına başvurun.
Araçların ne zaman çalıştığını algılama
VEYA PowerShell komutları kullanılırken dotnet-ef
EF Core uygulama kodunu çalıştırır. Bazen uygunsuz kodun tasarım zamanında yürütülmesini önlemek için bu durumu algılamak gerekebilir. Örneğin, başlangıçta geçişleri otomatik olarak uygulayan kodun bunu tasarım zamanında yapmaması gerekir. EF7'de bu, bayrağı kullanılarak EF.IsDesignTime
algılanabilir:
if (!EF.IsDesignTime)
{
await context.Database.MigrateAsync();
}
EF Core, uygulama kodunun IsDesignTime
true
araçlar adına ne zaman çalıştırılacağına ayarlar.
Proxy'ler için performans geliştirmeleri
EF Core, yavaş yükleme ve değişiklik izleme için dinamik olarak oluşturulan proxy'leri destekler. EF7, bu proxy'leri kullanırken iki performans geliştirmesi içerir:
- Proxy türleri artık lazily oluşturulur. Bu, ara sunucuları kullanırken ilk model oluşturma süresinin EF7 ile EF Core 6.0'dan çok daha hızlı olabileceği anlamına gelir.
- Proxy'ler artık derlenmiş modellerle kullanılabilir.
449 varlık türü, 6390 özellik ve 720 ilişki içeren bir model için bazı performans sonuçları aşağıdadır.
Senaryo | Metot | Ortalama | Hata | StdDev |
---|---|---|---|---|
Proxy'ler olmadan EF Core 6.0 | TimeToFirstQuery | 1,085 sn | 0,0083 sn | 0,0167 sn |
Değişiklik izleme proxy'leri ile EF Core 6.0 | TimeToFirstQuery | 13.01 sn | 0,2040 sn | 0,4110 sn |
Proxy'ler olmadan EF Core 7.0 | TimeToFirstQuery | 1,442 sn | 0,0134 sn | 0,0272 sn |
Değişiklik izleme proxy'leri ile EF Core 7.0 | TimeToFirstQuery | 1,446 sn | 0,0160 sn | 0,0323 sn |
Değişiklik izleme proxy'leri ve derlenmiş model içeren EF Core 7.0 | TimeToFirstQuery | 0,162 sn | 0,0062 sn | 0,0125 sn |
Bu durumda değişiklik izleme proxy'lerine sahip bir model, EF7'de EF Core 6.0 ile mümkün olandan 80 kat daha hızlı bir şekilde ilk sorguyu yürütmeye hazır olabilir.
Birinci sınıf Windows Forms veri bağlama
Windows Forms ekibi, Visual Studio Designer deneyiminde bazı harika geliştirmeler yapıyor. Bu, EF Core ile iyi tümleşen veri bağlamaya yönelik yeni deneyimler içerir.
Kısaca, yeni deneyim bir ObjectDataSourceoluşturmak için Visual Studio U.I. sağlar:
Bu işlem daha sonra basit bir kodla EF Core'a DbSet
bağlanabilir:
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;
}
}
Eksiksiz bir izlenecek yol ve indirilebilir WinForms örnek uygulaması için bkz. Windows Forms ile Çalışmaya Başlama.