EF Core 9의 새로운 기능
EF Core 9(EF9)는 EF Core 8 이후의 다음 릴리스이며 2024년 11월에 출시될 예정입니다.
EF9는 모든 최신 EF9 기능 및 API 조정을 포함하는 매일 빌드로 사용할 수 있습니다. 여기서 샘플은 이러한 일상적인 빌드를 사용합니다.
팁
GitHub에서 샘플 코드를 다운로드하여 샘플을 실행하고 디버그할 수 있습니다. 아래의 각 섹션은 해당 섹션과 관련된 소스 코드에 연결됩니다.
EF9는 .NET 8을 대상으로 하므로 .NET 8(LTS) 또는 .NET 9을사용할 수 있습니다.
팁
새로운 기능 문서는 미리 보기마다 업데이트됩니다. 모든 샘플은 EF9 일별 빌드를 사용하도록 설정되어 있으며 일반적으로 최신 미리 보기에 비해 완료 작업이 몇 주 더 걸립니다. 새로운 기능을 테스트할 때 부실한 비트에 대해 테스트를 수행하지 않도록 일별 빌드를 사용하는 것이 좋습니다.
Azure Cosmos DB for NoSQL
EF 9.0은 Azure Cosmos DB용 EF Core 공급자에 상당한 개선을 제공합니다. 공급자의 중요한 부분은 새로운 기능을 제공하고, 새로운 형태의 쿼리를 허용하며, 공급자를 Azure Cosmos DB 모범 사례와 더 잘 맞추기 위해 다시 작성되었습니다. 주요 상위 수준 개선 사항은 아래에 나열되어 있습니다. 전체 목록은 이 에픽 문제를 참조하세요.
Warning
공급자에 대한 개선의 일환으로 많은 영향을 미치는 호환성이 손상되는 변경이 이루어져야 했습니다. 기존 애플리케이션을 업그레이드하는 경우 호환성이 손상되는 변경 섹션을 주의 깊게 읽어 보세요.
파티션 키 및 문서 ID를 사용하여 쿼리 개선
Azure Cosmos DB 데이터베이스에 저장된 각 문서에는 고유한 리소스 ID가 있습니다. 또한 각 문서에는 데이터베이스를 효과적으로 크기 조정할 수 있도록 데이터의 논리적 분할을 결정하는 "파티션 키"가 포함될 수 있습니다. 파티션 키 선택에 대한 자세한 내용은 Azure Cosmos DB의 분할 및 수평 크기 조정을 참조하세요.
EF 9.0에서 Azure Cosmos DB 공급자는 LINQ 쿼리에서 파티션 키 비교를 식별하고 추출하여 쿼리가 관련 파티션으로만 전송되도록 하는 데 훨씬 더 적합합니다. 이렇게 하면 쿼리의 성능이 크게 향상되고 RU 요금이 감소할 수 있습니다. 예시:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
이 쿼리에서 공급자는 PartitionKey
에서 자동으로 비교를 인식합니다. 로그를 검사하면 다음이 표시됩니다.
Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")
WHERE
절에는 PartitionKey
가 포함되어 있지 않으며, 해당 비교가 "해제"되어 관련 파티션에 대해서만 쿼리를 실행하는 데 사용됩니다. 이전 버전에서는 많은 상황에서 비교가 WHERE
절에 남아 있어 모든 파티션에 대해 쿼리가 실행되어 비용이 증가하고 성능이 저하되었습니다.
또한 쿼리가 문서의 ID 속성에 대한 값을 제공하고 다른 쿼리 작업을 포함하지 않는 경우 공급자는 추가 최적화를 적용할 수 있습니다.
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
로그는 이 쿼리에 대해 다음을 보여 줍니다.
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
여기서는 SQL 쿼리가 전혀 전송되지 않습니다. 대신 공급자는 매우 효율적인 지점 읽기(ReadItem
API)를 수행하여 파티션 키와 ID가 지정된 문서를 직접 가져옵니다. Azure Cosmos DB에서 수행할 수 있는 가장 효율적이고 비용 효율적인 읽기 종류입니다. 지점 읽기에 대한 자세한 내용은 Azure Cosmos DB 설명서를 참조하세요.
파티션 키 및 지점 읽기를 사용하여 쿼리하는 방법에 대한 자세한 내용은 쿼리 설명서 페이지를 참조하세요.
계층적 파티션 키
팁
여기에 표시된 코드는 HierarchicalPartitionKeysSample.cs에서 가져온 것입니다.
Azure Cosmos DB는 원래 단일 파티션 키를 지원했지만 이후 파티션 키에서 최대 3개 수준의 계층 구조 사양을 통해 하위 분할을 지원하도록 분할 기능을 확장했습니다. EF Core 9는 계층적 파티션 키에 대한 모든 지원을 제공하므로 이 기능과 관련된 더 나은 성능 및 비용 절감을 활용할 수 있습니다.
파티션 키는 모델 빌드 API를 사용하여 일반적으로 DbContext.OnModelCreating에 지정됩니다. 파티션 키의 각 수준에 대한 엔터티 형식에 매핑된 속성이 있어야 합니다. 예를 들어 UserSession
엔터티 형식을 고려합니다.
public class UserSession
{
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
다음 코드는 TenantId
, UserId
및 SessionId
속성을 사용하여 3단계 파티션 키를 지정합니다.
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
팁
이 파티션 키 정의는 Azure Cosmos DB 설명서의 계층적 파티션 키 선택에 제공된 예를 따릅니다.
EF Core 9부터 모든 매핑된 형식의 속성을 파티션 키에서 사용할 수 있는 방법에 유의해야 합니다. bool
및 숫자 형식(예: int SessionId
속성)의 경우 값은 파티션 키에서 직접 사용됩니다. Guid UserId
속성과 같은 다른 형식은 자동으로 문자열로 변환됩니다.
쿼리할 때 EF는 쿼리에서 파티션 키 값을 자동으로 추출하고 Azure Cosmos DB 쿼리 API에 적용하여 쿼리가 가능한 가장 적은 수의 파티션으로 적절하게 제한되도록 합니다. 예를 들어 계층 구조의 세 파티션 키 값을 모두 제공하는 다음 LINQ 쿼리를 고려합니다.
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
이 쿼리를 실행할 때 EF Core는 , userId
및 매개 변수의 tenantId
값을 추출하고 sessionId
이를 파티션 키 값으로 Azure Cosmos DB 쿼리 API에 전달합니다. 예를 들어, 위 쿼리 실행의 로그를 참조하세요.
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
파티션 키 비교는 WHERE
절에서 제거되었으며 대신 효율적인 실행을 위한 파티션 키로 사용됩니다. ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
자세한 내용은 파티션 키를 사용하여 쿼리하는 방법에 대한 설명서를 참조하세요.
대폭 향상된 LINQ 쿼리 기능
EF 9.0에서는 Azure Cosmos DB 공급자의 LINQ 변환 기능이 크게 확장되었으며 이제 공급자가 훨씬 더 많은 쿼리 형식을 실행할 수 있습니다. 쿼리 개선 사항의 전체 목록이 너무 길어 나열할 수 없지만 주요 강조 내용은 다음과 같습니다.
- EF의 기본 컬렉션에 대한 모든 지원을 통해 int 또는 문자열과 같은 컬렉션에 대해 LINQ 쿼리를 수행할 수 있습니다. 자세한 내용은 EF8: 기본 컬렉션의 새로운 기능을 참조하세요.
- 기본이 아닌 컬렉션에 대한 임의 쿼리를 지원합니다.
- 이제 컬렉션,
Length
/Count
,ElementAt
Contains
등으로 인덱싱하는 추가 LINQ 연산자가 많이 지원됩니다. - 와 같은
Count
Sum
집계 연산자 지원 - 추가 함수 번역(지원되는 번역의 전체 목록에 대한 함수 매핑 설명서 참조):
- 구성 요소 멤버
DateTime.Year
(,DateTimeOffset.Month
...)에 대한DateTime
DateTimeOffset
번역입니다. - 이제
EF.Functions.IsDefined
및EF.Functions.CoalesceUndefined
를 통해undefined
값을 처리할 수 있습니다. string.Contains
,StartsWith
및EndsWith
는 이제StringComparison.OrdinalIgnoreCase
를 지원합니다.
- 구성 요소 멤버
쿼리 개선 사항의 전체 목록은 이 문제를 참조하세요.
Azure Cosmos DB 및 JSON 표준에 맞게 향상된 모델링
EF 9.0은 JSON 기반 문서 데이터베이스에 대한 보다 자연스러운 방법으로 Azure Cosmos DB 문서에 매핑되며 문서에 액세스하는 다른 시스템과 상호 운용하는 데 도움이 됩니다. 이 경우 호환성이 손상되는 변경이 수반되지만 모든 경우에 9.0 이전 동작으로 되돌릴 수 있는 API가 존재합니다.
판별자 없이 간소화된 id
속성
첫째, 이전 버전의 EF는 JSON id
속성에 판별자 값을 삽입하여 다음과 같은 문서를 생성했습니다.
{
"id": "Blog|1099",
...
}
이는 서로 다른 형식의 문서(예: 블로그 및 게시물)와 동일한 키 값(1099)이 동일한 컨테이너 파티션 내에 존재할 수 있도록 하기 위해 수행되었습니다. EF 9.0부터 id
속성에는 키 값만 포함됩니다.
{
"id": 1099,
...
}
이는 JSON에 매핑하는 보다 자연스러운 방법이며 외부 도구 및 시스템이 EF에서 생성된 JSON 문서와 보다 쉽게 상호 작용할 수 있도록 합니다. 이러한 외부 시스템은 일반적으로 .NET 형식에서 파생되는 EF 판별자 값을 인식하지 못합니다.
EF가 더 이상 이전 id
형식의 기존 문서를 쿼리할 수 없으므로 이는 호환성이 손상되는 변경입니다. 이전 동작으로 되돌리기 위해 API가 도입되었습니다. 자세한 내용은 호환성이 손상되는 변경 정보 및 설명서를 참조하세요.
판별자 속성 이름이 $type
으로 변경됨
기본 판별자 속성의 이전 이름은 Discriminator
였습니다. EF 9.0은 기본값을 $type
으로 변경합니다.
{
"id": 1099,
"$type": "Blog",
...
}
이는 JSON 다형성에 대한 새로운 표준을 따르며 다른 도구와의 상호 운용성을 향상합니다. 예를 들어. NET의 System.Text.Json은 $type
을 기본 판별자 속성 이름(docs)으로 사용하여 다형성도 지원합니다.
EF는 더 이상 이전 판별자 속성 이름으로 기존 문서를 쿼리할 수 없으므로 이는 호환성이 손상되는 변경입니다. 이전 명명으로 되돌리는 방법에 대한 자세한 내용은 호환성이 손상되는 변경 정보를 참조하세요.
벡터 유사성 검색(미리 보기)
Azure Cosmos DB는 이제 벡터 유사성 검색에 대한 미리 보기 지원을 제공합니다. 벡터 검색은 AI, 의미 체계 검색 및 기타를 비롯한 일부 애플리케이션 유형의 기본 부분입니다. Azure Cosmos DB를 사용하면 나머지 데이터와 함께 문서에 직접 벡터를 저장할 수 있습니다. 즉, 단일 데이터베이스에 대해 모든 쿼리를 수행할 수 있습니다. 이렇게 하면 아키텍처가 상당히 간소화되고 스택에 전용 벡터 데이터베이스 솔루션이 추가로 필요하지 않습니다. Azure Cosmos DB 벡터 검색 에 대한 자세한 내용은 설명서를 참조하세요.
Azure Cosmos DB 컨테이너가 제대로 설정되면 EF를 통해 벡터 검색을 사용하는 것은 벡터 속성을 추가하고 구성하는 간단한 문제입니다.
public class Blog
{
...
public float[] Vector { get; set; }
}
public class BloggingContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}
완료되면 LINQ 쿼리의 EF.Functions.VectorDistance()
함수를 사용하여 벡터 유사성 검색을 수행합니다.
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
자세한 내용은 벡터 검색에 대한 설명서를 참조하세요.
페이지 매김 지원
이제 Azure Cosmos DB 공급자는 연속 토큰을 통해 쿼리 결과를 페이지 매김할 수 있습니다. 이 토큰은 기존의 사용 Skip
Take
및 사용보다 훨씬 효율적이고 비용 효율적입니다.
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
새 ToPageAsync
연산자는 나중에 쿼리를 효율적으로 다시 시작하는 데 사용할 수 있는 연속 토큰을 노출하고 다음 10개 항목을 가져오는 CosmosPage
를 반환합니다.
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
자세한 내용은 페이지 매김에 대한 설명서 섹션을 참조하세요.
보다 안전한 SQL 쿼리를 위한 FromSql
Azure Cosmos DB 공급자는 .를 통한 FromSqlRawSQL 쿼리를 허용했습니다. 그러나 해당 API는 사용자가 제공한 데이터가 SQL에 보간되거나 연결될 때 SQL 삽입 공격에 취약할 수 있습니다. 이제 EF 9.0에서 매개 변수가 있는 데이터를 항상 SQL 외부의 매개 변수로 통합하는 새 FromSql
메서드를 사용할 수 있습니다.
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
자세한 내용은 페이지 매김에 대한 설명서 섹션을 참조하세요.
역할 기반 액세스
Azure Cosmos DB for NoSQL에는 기본 제공 RBAC(역할 기반 액세스 제어) 시스템이 포함되어 있습니다. 이제 모든 데이터 평면 작업에 대해 EF9에서 지원됩니다. 그러나 Azure Cosmos DB SDK는 Azure Cosmos DB의 관리 평면 작업에 RBAC를 지원하지 않습니다. RBAC 대신 EnsureCreatedAsync
Azure Management API를 사용합니다.
이제 동기 I/O가 기본적으로 차단됨
Azure Cosmos DB for NoSQL은 애플리케이션 코드의 동기(차단) API를 지원하지 않습니다. 이전에는 EF가 비동기 호출에서 사용자를 차단하여 이를 마스킹했습니다. 그러나 이 두 가지 모두 동기 I/O 사용을 장려하며, 이는 잘못된 습관이며 교착 상태를 초래할 수 있습니다. 따라서 EF 9부터 동기 액세스를 시도할 때 예외가 throw됩니다. 예시:
동기 I/O는 경고 수준을 적절하게 구성하여 현재로서는 계속 사용할 수 있습니다. 예를 들어, DbContext
형식의 OnConfiguring
에 다음을 입력합니다.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
그러나 EF 11에서 동기화 지원을 완전히 제거할 계획이므로 가능한 한 빨리 ToListAsync
및 SaveChangesAsync
와 같은 비동기 메서드를 사용하도록 업데이트를 시작하세요!
AOT 및 사전 컴파일된 쿼리
Warning
NativeAOT 및 쿼리 미리 컴파일은 매우 실험적인 기능이며 프로덕션 사용에는 아직 적합하지 않습니다. 아래에 설명된 지원은 최종 기능에 대한 인프라로 간주되어야 하며, 이는 EF 10과 함께 릴리스될 가능성이 높습니다. 현재 지원을 실험하고 환경에 대해 보고하는 것이 좋지만 프로덕션 환경에서 EF NativeAOT 애플리케이션을 배포하지 않는 것이 좋습니다.
EF 9.0은 .NET 네이티브AOT에 대한 초기 실험적 지원을 제공하므로 EF를 사용하여 데이터베이스에 액세스하는 미리 컴파일된 애플리케이션을 게시할 수 있습니다. NativeAOT 모드에서 LINQ 쿼리를 지원하기 위해 EF는 쿼리 미리 컴파일을 사용합니다. 이 메커니즘은 EF LINQ 쿼리를 정적으로 식별하고 각 특정 쿼리를 실행하는 코드를 포함하는 C# 인터셉터를 생성합니다. 이렇게 하면 애플리케이션이 시작될 때마다 LINQ 쿼리를 SQL로 처리하고 컴파일하는 작업을 많이 수행하지 않아 애플리케이션의 시작 시간이 크게 단축됩니다. 대신 각 쿼리의 인터셉터에는 데이터베이스 결과를 .NET 개체로 구체화하는 최적화된 코드뿐만 아니라 해당 쿼리에 대한 최종 SQL이 포함됩니다.
예를 들어 다음 EF 쿼리를 사용하는 프로그램이 제공됩니다.
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF는 쿼리 실행을 인수하는 C# 인터셉터를 프로젝트에 생성합니다. 프로그램이 시작될 때마다 쿼리를 처리하고 SQL로 변환하는 대신 인터셉터에 SQL이 포함됩니다(이 경우 SQL Server의 경우). 프로그램을 훨씬 더 빠르게 시작할 수 있습니다.
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
또한 동일한 인터셉터에는 데이터베이스 결과에서 .NET 개체를 구체화하는 코드가 포함되어 있습니다.
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
이렇게 하면 다른 새로운 .NET 기능인 안전하지 않은 접근자를 사용하여 데이터베이스의 데이터를 개체의 개인 필드에 삽입합니다.
NativeAOT에 관심이 있고 최첨단 기능을 실험하고 싶다면 시도해 보세요! 이 기능은 불안정한 것으로 간주되어야 하며 현재 많은 제한 사항이 있습니다. 이를 안정화하고 EF 10의 프로덕션 사용에 더 적합할 것으로 기대합니다.
자세한 내용은 NativeAOT 설명서 페이지를 참조하세요.
LINQ 및 SQL 변환
모든 릴리스와 마찬가지로 EF9에는 LINQ 쿼리 기능이 크게 향상되었습니다. 새 쿼리를 변환할 수 있으며, 향상된 성능과 가독성을 위해 지원되는 시나리오에 대한 많은 SQL 변환이 개선되었습니다.
개선 사항의 수는 여기에 모두 나열하기에는 너무 많습니다. 아래에서는 몇 가지 더 중요한 개선 사항이 강조 표시되어 있습니다. 9.0에서 수행된 작업의 전체 목록은 이 문제를 참조하세요.
EF Core에서 생성되는 SQL을 최적화하는 데 수많은 고품질 기여를 해주신 Andrea Canciani(@ranma42)를 호출하고자 합니다.
복합 형식: GroupBy 및 ExecuteUpdate 지원
GroupBy
팁
여기에 표시된 코드는 ComplexTypesSample.cs에서 가져온 것입니다.
EF9는 복합 형식 인스턴스별 그룹화를 지원합니다. 예시:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF는 이를 복합 형식의 각 멤버별로 그룹화하는 것으로 변환하며, 이는 값 개체로서 복합 형식의 의미 체계에 맞춰 조정됩니다. 예를 들어, Azure SQL에서는 다음과 같습니다.
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExecuteUpdate
팁
여기에 표시된 코드는 ExecuteUpdateSample.cs에서 제공됩니다.
마찬가지로 EF9 ExecuteUpdate
에서도 복합 형식 속성을 허용하도록 개선되었습니다. 그러나 복합 형식의 각 멤버는 명시적으로 지정되어야 합니다. 예시:
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));
이렇게 하면 복합 형식에 매핑된 각 열을 업데이트하는 SQL이 생성됩니다.
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
이전에는 ExecuteUpdate
호출에서 복합 형식의 다양한 속성을 수동으로 나열해야 했습니다.
SQL에서 불필요한 요소 정리
이전에는 EF에서 실제로 필요하지 않은 요소가 포함된 SQL을 생성하기도 했습니다. 대부분의 경우 이러한 항목은 SQL 처리의 이전 단계에서 필요하고 남아 있을 수 있습니다. EF9는 이제 대부분의 요소를 정리하여 더 간결하고 경우에 따라 더 효율적인 SQL을 생성합니다.
테이블 정리
첫 번째 예제로, EF에서 생성된 SQL에는 쿼리에 실제로 필요하지 않은 테이블에 대한 JOIN이 포함되어 있는 경우가 있습니다. TPT(형식당 하나의 테이블) 상속 매핑을 사용하는 다음 모델을 고려합니다.
public class Order
{
public int Id { get; set; }
...
public Customer Customer { get; set; }
}
public class DiscountedOrder : Order
{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...
public List<Order> Orders { get; set; }
}
public class BlogContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}
그런 다음 다음 쿼리를 실행하여 주문이 하나 이상 있는 모든 고객을 가져옵니다.
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8에서 다음 SQL을 생성했습니다.
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])
쿼리에 열이 참조되지 않았음에도 불구하고 DiscountedOrders
테이블에 대한 조인이 포함되어 있습니다. EF9는 조인 없이 정리된 SQL을 생성합니다.
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
프로젝션 정리
마찬가지로 다음 쿼리를 살펴보겠습니다.
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
EF8에서 이 쿼리는 다음 SQL을 생성했습니다.
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
외부 SELECT 식은 단순히 행 수를 계산하기 때문에 하위 쿼리에서는 [o].[Id]
프로젝션이 필요하지 않습니다. EF9는 대신 다음을 생성합니다.
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... 그리고 프로젝션이 비어 있습니다. 이는 별로 많아 보이지 않을 수 있지만 경우에 따라 SQL을 상당히 단순화할 수 있습니다. 테스트의 SQL 변경 내용 중 일부를 스크롤하여 효과를 확인할 수 있습니다.
GREATEST/LEAST 관련 번역
팁
여기에 표시된 코드는 LeastGreatestSample.cs에서 가져온 것입니다.
GREATEST
및 LEAST
SQL 함수를 사용하는 몇 가지 새로운 변환이 도입되었습니다.
Important
GREATEST
및 LEAST
함수는 2022 버전의 SQL Server/Azure SQL Database에 도입되었습니다. Visual Studio 2022는 기본적으로 SQL Server 2019를 설치합니다. EF9에서 이러한 새로운 번역을 시험해 보려면 SQL Server Developer Edition 2022를 설치하는 것이 좋습니다.
예를 들어, Math.Max
또는 Math.Min
을 사용하는 쿼리는 이제 각각 GREATEST
및 LEAST
를 사용하는 Azure SQL을 위해 번역됩니다. 예시:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
이 쿼리는 SQL Server 2022에 대해 실행되는 EF9를 사용할 때 다음 SQL로 변환됩니다.
SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >
Math.Min
및 Math.Max
는 기본 컬렉션의 값에도 사용될 수 있습니다. 예시:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
이 쿼리는 SQL Server 2022에 대해 실행되는 EF9를 사용할 때 다음 SQL로 변환됩니다.
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
마지막으로 SQL에서 RelationalDbFunctionsExtensions.Least
및 RelationalDbFunctionsExtensions.Greatest
를 사용하여 Least
또는 Greatest
함수를 직접 호출할 수 있습니다. 예시:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
이 쿼리는 SQL Server 2022에 대해 실행되는 EF9를 사용할 때 다음 SQL로 변환됩니다.
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
쿼리 매개 변수화 적용 또는 방지
팁
여기에 표시된 코드는 QuerySample.cs에서 가져온 것입니다.
일부 특별한 경우를 제외하고 EF Core는 LINQ 쿼리에 사용되는 변수를 매개 변수화하지만 생성된 SQL에 상수를 포함합니다. 예를 들어, 다음 쿼리 방법을 살펴보세요.
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
이는 Azure SQL을 사용할 때 다음 SQL 및 매개 변수로 변환됩니다.
Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0
EF는 ".NET 블로그"에 대한 SQL에서 상수를 만들었습니다. 이 값은 쿼리마다 변경되지 않기 때문입니다. 상수를 사용하면 쿼리 계획을 만들 때 데이터베이스 엔진에서 이 값을 검사할 수 있으므로 잠재적으로 쿼리가 더 효율적이 됩니다.
반면, 동일한 쿼리가 id
에 대해 여러 다른 값으로 실행될 수 있으므로 id
의 값은 매개 변수화됩니다. 이 경우 상수를 만들면 id
값만 다른 많은 쿼리로 인해 쿼리 캐시가 오염됩니다. 이는 데이터베이스의 전반적인 성능에 매우 좋지 않습니다.
일반적으로 이러한 기본값은 변경하면 안 됩니다. 그러나 EF Core 8.0.2에는 매개 변수가 기본적으로 사용되는 경우에도 EF가 상수를 사용하도록 하는 EF.Constant
메서드가 도입되었습니다. 예시:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
이제 번역에는 id
값에 대한 상수가 포함됩니다.
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
EF.Parameter
메서드
반대로 수행되도록 EF9에 EF.Parameter
메서드가 도입되었습니다. 즉, 값이 코드에서 상수인 경우에도 EF가 매개 변수를 사용하도록 합니다. 예시:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
이제 번역에는 ".NET Blog" 문자열에 대한 매개 변수가 포함됩니다.
Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
매개 변수가 있는 기본 컬렉션
EF8은 기본 컬렉션을 사용하는 일부 쿼리가 번역되는 방식을 변경했습니다. LINQ 쿼리에 매개 변수가 있는 기본 컬렉션이 포함된 경우 EF는 해당 내용을 JSON으로 변환하고 쿼리에 단일 매개 변수 값으로 전달합니다.
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
그러면 SQL Server에서 다음과 같은 번역이 수행됩니다.
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
이렇게 하면 다른 매개 변수가 있는 컬렉션에 대해 동일한 SQL 쿼리를 사용할 수 있지만(매개 변수 값만 변경됨) 데이터베이스가 쿼리를 최적으로 계획할 수 없으므로 성능 문제가 발생할 수 있습니다. 이 메서드를 EF.Constant
사용하여 이전 번역으로 되돌릴 수 있습니다.
다음 쿼리는 해당 효과를 사용합니다 EF.Constant
.
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
결과 SQL은 다음과 같습니다.
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
또한 EF9에는 모든 쿼리에 TranslateParameterizedCollectionsToConstants
대한 기본 컬렉션 매개 변수화를 방지하는 데 사용할 수 있는 컨텍스트 옵션이 도입되었습니다. 기본 컬렉션의 매개 변수화를 명시적으로 강제하는 TranslateParameterizedCollectionsToParameters
보완 기능도 추가했습니다(기본 동작임).
팁
메서드는 EF.Parameter
컨텍스트 옵션을 재정의합니다. 대부분의 쿼리(전부는 아님)에 대한 기본 컬렉션의 매개 변수화를 방지하려면 컨텍스트 옵션을 TranslateParameterizedCollectionsToConstants
설정하고 매개 변수화하려는 쿼리 또는 개별 변수에 사용할 EF.Parameter
수 있습니다.
인라인 상관되지 않은 하위 쿼리
팁
여기에 표시된 코드는 QuerySample.cs에서 가져온 것입니다.
EF8에서는 다른 쿼리에서 참조되는 IQueryable이 별도의 데이터베이스 왕복으로 실행될 수 있습니다. 예를 들어 다음 LINQ 쿼리를 생각해 봅니다.
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArray();
EF8에서는 dotnetPosts
에 대한 쿼리가 1회 왕복으로 실행된 후 최종 결과가 두 번째 쿼리로 실행됩니다. 예를 들어 SQL Server에서는 다음과 같이 표시됩니다.
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
EF9에서는 dotnetPosts
의 IQueryable
이 인라인되어 단일 데이터베이스 왕복이 발생합니다.
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
SQL Server의 하위 쿼리 및 집계에 대한 함수 집계
EF9는 하위 쿼리 또는 다른 집계 함수를 통해 구성된 집계 함수를 사용하여 일부 복잡한 쿼리의 변환을 향상시킵니다. 다음은 이러한 쿼리의 예입니다.
var latestPostsAverageRatingByLanguage = await context.Blogs
.Select(x => new
{
x.Language,
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.LatestPostRating))
.ToListAsync();
Select
먼저 SQL로 변환할 때 하위 쿼리가 필요한 각 Post
쿼리를 계산 LatestPostRating
합니다. 쿼리의 뒷부분에서 이러한 결과는 작업을 사용하여 Average
집계됩니다. 결과 SQL은 SQL Server에서 실행될 때 다음과 같이 표시됩니다.
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
이전 버전에서 EF Core는 하위 쿼리에 직접 집계 작업을 적용하려고 하는 유사한 쿼리에 대해 잘못된 SQL을 생성합니다. SQL Server에서는 허용되지 않으며 예외가 발생합니다. 다른 집계에 대해 집계를 사용하는 쿼리에 동일한 원칙이 적용됩니다.
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();
참고 항목
이 변경 내용은 하위 쿼리(또는 다른 집계)에 대한 집계를 지원하는 Sqlite에 영향을 주지 않으며(APPLY
)를 지원하지 LATERAL JOIN
않습니다. 다음은 Sqlite에서 실행되는 첫 번째 쿼리에 대한 SQL입니다.
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
Count != 0을 사용하는 쿼리가 최적화됨
팁
여기에 표시된 코드는 QuerySample.cs에서 가져온 것입니다.
EF8에서는 다음 LINQ 쿼리가 SQL COUNT
함수를 사용하도록 변환되었습니다.
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
이제 EF9는 EXISTS
를 사용하여 보다 효율적인 변환을 생성합니다.
SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")
nullable 값에 대한 비교 작업에 대한 C# 의미 체계
EF8에서는 일부 시나리오에서 nullable 요소 간의 비교가 올바르게 수행되지 않았습니다. C#에서는 피연산자 중 하나 또는 둘 모두가 null이면 비교 연산자의 결과는 false입니다. 그렇지 않으면 포함된 피연산자 값을 비교합니다. EF8에서는 데이터베이스 null 의미 체계를 사용하여 비교를 변환하는 데 사용했습니다. 이렇게 하면 LINQ to Objects를 사용하는 유사한 쿼리와 다른 결과가 생성됩니다. 또한 필터와 프로젝션에서 비교가 수행되었을 때 다른 결과를 생성합니다. 일부 쿼리는 Sql Server와 Sqlite/Postgres 간에도 다른 결과를 생성합니다.
예를 들어 쿼리는
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
다음 SQL을 생성합니다.
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
이는 NullableIntOne
또는 NullableIntTwo
가 null로 설정된 엔터티를 필터링합니다.
EF9에서는 다음을 생성합니다.
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)
프로젝션에서 수행된 유사한 비교:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
그러면 다음과 같은 SQL이 생성됩니다.
SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]
NullableIntOne
또는 NullableIntTwo
가 null로 설정된 엔터티에 대해 false
를 반환합니다(C#에서 예상되는 true
가 아님). 생성된 Sqlite에서 동일한 시나리오를 실행합니다.
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
NullableIntOne
또는 NullableIntTwo
가 null인 경우 변환이 null
값을 생성하므로 Nullable object must have a value
예외가 발생합니다.
이제 EF9는 이러한 시나리오를 올바르게 처리하여 LINQ to Objects 및 여러 공급자와 일치하는 결과를 생성합니다.
이 개선 사항은 @ranma42에 의해 제공되었습니다. 대단히 고맙습니다!
LINQ 연산자 Order
OrderDescending
변환
EF9를 사용하면 LINQ 간소화된 순서 지정 작업(Order
및 OrderDescending
)을 변환할 수 있습니다. 이러한 작업은 인수와 OrderBy
/OrderByDescending
비슷하지만 필요하지는 않습니다. 대신 기본 순서를 적용합니다. 엔터티의 경우 기본 키 값을 기반으로 정렬하고 다른 형식의 경우 값 자체에 따라 순서를 지정합니다.
다음은 간소화된 순서 지정 연산자를 활용하는 예제 쿼리입니다.
var orderOperation = await context.Blogs
.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();
이 쿼리는 다음과 같습니다.
var orderByEquivalent = await context.Blogs
.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
})
.ToListAsync();
다음 SQL을 생성합니다.
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
참고 항목
Order
및 OrderDescending
메서드는 엔터티, 복합 형식 또는 스칼라 컬렉션에 대해서만 지원됩니다. 여러 속성을 포함하는 익명 형식의 컬렉션과 같이 더 복잡한 프로젝션에서는 작동하지 않습니다.
이 향상된 기능은 EF 팀 동창 @bricelam 기여했습니다. 대단히 고맙습니다!
논리 부정 연산자 변환 개선(!)
EF9는 SQL CASE/WHEN
, COALESCE
부정 및 기타 다양한 구문을 중심으로 많은 최적화를 제공합니다. 이들 대부분은 Andrea Canciani (@ranma42)에 의해 기여되었습니다 . 아래에서는 논리적 부정에 대한 이러한 최적화 중 몇 가지에 대해 자세히 설명합니다.
다음 쿼리를 살펴보겠습니다.
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
EF8에서는 다음 SQL을 생성합니다.
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
EF9에서는 비교에 NOT
작업을 "푸시"합니다.
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
SQL Server에 적용할 수 있는 또 다른 예는 부정 조건부 작업입니다.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
EF8에서는 중첩된 CASE
블록을 생성하는 데 사용됩니다.
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
EF9에서 중첩을 제거했습니다.
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
SQL Server에서 부정 부울 속성을 프로젝션하는 경우:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
비교는 SQL Server 쿼리에서 직접 프로젝션에 표시될 수 없으므로 EF8은 CASE
블록을 생성합니다.
SELECT [p].[Title], CASE
WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]
EF9에서 이 변환은 간소화되었으며 이제 비트 NOT()를~
사용합니다.
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Azure SQL 및 Azure Synapse에 대한 향상된 지원
EF9를 사용하면 대상으로 지정되는 SQL Server 유형을 지정할 때 유연성이 높아집니다. EF를 UseSqlServer
구성하는 대신 이제 지정 UseAzureSql
하거나 UseAzureSynapse
.
이를 통해 EF는 Azure SQL 또는 Azure Synapse를 사용할 때 더 나은 SQL을 생성할 수 있습니다. EF는 데이터베이스 관련 기능(예: Azure SQL의 JSON 전용 형식)을 활용하거나 해당 제한 사항을 해결할 수 있습니다(예:ESCAPE
Azure Synapse에서 사용하는 LIKE
경우 절을 사용할 수 없음).
다른 쿼리 기능 향상
- EF8에 도입된 기본 컬렉션 쿼리 지원은 모든
ICollection<T>
형식을 지원하도록 확장되었습니다. 이는 매개 변수 및 인라인 컬렉션에만 적용됩니다. 엔터티의 일부인 기본 컬렉션은 여전히 배열, 목록으로 제한되며 EF9에서는 읽기 전용 배열/목록으로 제한됩니다. - 쿼리 결과를
HashSet
으로 반환하는 새ToHashSetAsync
함수입니다(#30033, @wertzui 기여). TimeOnly.FromDateTime
및FromTimeSpan
이 이제 SQL Server에서 변환됩니다(#33678).- 열거형에 대한
ToString
이 이제 변환됩니다(#33706, @Danevandy99 기여). string.Join
은 이제 SQL Server의 비집계 컨텍스트에서 CONCAT_WS로 변환됩니다(#28899).EF.Functions.PatIndex
는 이제 패턴이 처음 나타나는 시작 위치를 반환하는 SQL ServerPATINDEX
함수로 변환됩니다(#33702, @smnsht).Sum
및Average
는 이제 SQLite에서 소수점에 대해 작동합니다(#33721, @ranma42 기여).string.StartsWith
및EndsWith
에 대한 수정 및 최적화(#31482).Convert.To*
메서드는 이제object
형식의 인수를 수락할 수 있습니다(#33891, @imangd 기여).- XOR(Exclusive-Or) 작업은 이제 SQL Server에서 변환됩니다(#34071, @ranma42 제공).
- null 허용 여부 및 작업에 대한
COLLATE
최적화(#34263, @ranma42 제공)AT TIME ZONE
- 오버 및 설정 작업에 대한
DISTINCT
최적화(#34381, @ranma42 제공)EXISTS
IN
위의 내용은 EF9에서 더 중요한 쿼리 개선 사항 중 일부에 불과하며, 전체 목록은 이 문제를 참조하세요.
마이그레이션
동시 마이그레이션에 대한 보호
EF9는 데이터베이스가 손상된 상태로 남을 수 있으므로 동시에 발생하는 여러 마이그레이션 실행으로부터 보호하는 잠금 메커니즘을 도입했습니다. 권장되는 방법을 사용하여 마이그레이션을 프로덕션 환경에 배포할 때는 발생하지 않지만, 이 메서드를 사용하여 DbContext.Database.Migrate()
런타임에 마이그레이션을 적용하는 경우 발생할 수 있습니다. 애플리케이션 시작의 일부가 아니라 배포 시 마이그레이션을 적용하는 것이 좋지만, 이로 인해 더 복잡한 애플리케이션 아키텍처(예: .NET Aspire 프로젝트를 사용하는 경우)가 발생할 수 있습니다.
참고 항목
Sqlite 데이터베이스를 사용하는 경우 이 기능과 관련된 잠재적인 문제를 참조하세요.
트랜잭션 내에서 여러 마이그레이션 작업을 실행할 수 없는 경우 경고
마이그레이션 중에 수행되는 대부분의 작업은 트랜잭션으로 보호됩니다. 이렇게 하면 어떤 이유로 마이그레이션이 실패하는 경우 데이터베이스가 손상된 상태로 끝나지 않습니다. 그러나 일부 작업은 트랜잭션에 래핑되지 않습니다(예: SQL Server 메모리 최적화 테이블에 대한 작업 또는 데이터베이스 데이터 정렬 수정과 같은 데이터베이스 변경 작업). 마이그레이션 실패 시 데이터베이스가 손상되지 않도록 하려면 별도의 마이그레이션을 사용하여 이러한 작업을 격리하여 수행하는 것이 좋습니다. 이제 EF9는 마이그레이션에 트랜잭션에 래핑할 수 없는 여러 작업이 포함된 시나리오를 감지하고 경고를 실행합니다.
향상된 데이터 시드
EF9는 데이터베이스를 초기 데이터로 채우는 데이터 시드를 수행하는 편리한 방법을 도입했습니다. DbContextOptionsBuilder
에는 UseSeeding
DbContext가 초기화될 때(의 EnsureCreatedAsync
일부로) 실행되는 메서드가 UseAsyncSeeding
포함됩니다.
참고 항목
애플리케이션이 이전에 실행된 경우 데이터베이스에 샘플 데이터가 이미 포함되어 있을 수 있습니다(컨텍스트의 첫 번째 초기화에 추가되었을 수 있음). 따라서 UseSeeding
UseAsyncSeeding
데이터베이스를 채우기 전에 데이터가 있는지 확인해야 합니다. 이 작업은 간단한 EF 쿼리를 실행하여 수행할 수 있습니다.
다음은 이러한 메서드를 사용하는 방법의 예입니다.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
자세한 내용은 여기를 참조하세요.
기타 마이그레이션 개선 사항
- 기존 테이블을 SQL Server 임시 테이블로 변경하는 경우 마이그레이션 코드 크기가 크게 줄어듭니다.
모델 빌드
자동 컴파일된 모델
팁
여기에 표시된 코드는 NewInEFCore9.CompiledModels 샘플에서 가져온 것입니다.
컴파일된 모델은 대규모 모델, 즉 엔터티 형식 개수가 100~1000개인 애플리케이션의 작동 시간을 단축할 수 있습니다. 이전 버전의 EF Core에서는 명령줄을 사용하여 컴파일된 모델을 수동으로 생성해야 했습니다. 예시:
dotnet ef dbcontext optimize
명령을 실행한 후 EF Core에 컴파일된 모델을 사용하도록 지시하려면 .UseModel(MyCompiledModels.BlogsContextModel.Instance)
와 같은 줄을 OnConfiguring
에 추가해야 합니다.
EF9부터 애플리케이션의 DbContext
형식이 컴파일된 모델과 동일한 프로젝트/어셈블리에 있는 경우 이 .UseModel
줄이 더 이상 필요하지 않습니다. 대신 컴파일된 모델이 자동으로 검색되어 사용됩니다. 이는 모델을 빌드할 때마다 EF 로그를 통해 확인할 수 있습니다. 간단한 애플리케이션을 실행하면 애플리케이션이 시작될 때 EF가 모델을 빌드하는 것을 보여 줍니다.
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
모델 프로젝트에서 dotnet ef dbcontext optimize
를 실행하면 출력은 다음과 같습니다.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
로그 출력에는 명령을 실행할 때 모델이 빌드되었다는 내용이 표시됩니다. 이제 다시 빌드한 후 코드를 변경하지 않고 애플리케이션을 다시 실행하면 출력은 다음과 같습니다.
Starting application...
Model loaded with 2 entity types.
컴파일된 모델이 자동으로 검색되어 사용되므로 애플리케이션을 시작할 때 모델이 빌드되지 않았습니다.
MSBuild 통합
위의 방식을 사용하면 엔터티 형식이나 DbContext
구성이 변경될 때 컴파일된 모델을 수동으로 다시 생성해야 합니다. 그러나 EF9는 모델 프로젝트가 빌드될 때 컴파일된 모델을 자동으로 업데이트할 수 있는 MSBuild 작업 패키지와 함께 제공됩니다. 시작하려면 Microsoft.EntityFrameworkCore.Tasks NuGet 패키지를 설치합니다. 예시:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
팁
사용 중인 EF Core 버전과 일치하는 위 명령의 패키지 버전을 사용합니다.
그런 다음 파일의 속성 및 EFScaffoldModelStage
설정을 통해 통합을 EFOptimizeContext
.csproj
사용하도록 설정합니다. 예시:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
이제 프로젝트를 빌드하면 빌드 시 컴파일된 모델이 빌드되고 있음을 나타내는 로깅을 볼 수 있습니다.
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
그리고 애플리케이션을 실행하면 컴파일된 모델이 검색되었으므로 모델이 다시 빌드되지 않음이 표시됩니다.
Starting application...
Model loaded with 2 entity types.
이제 모델이 변경될 때마다 프로젝트가 빌드되는 즉시 컴파일된 모델이 자동으로 다시 빌드됩니다.
자세한 내용은 MSBuild 통합을 참조 하세요.
읽기 전용 기본 컬렉션
팁
여기에 표시된 코드는 PrimitiveCollectionsSample.cs에서 제공됩니다.
EF8에서는 기본 형식의 배열 및 변경 가능한 목록 매핑에 대한 지원을 도입했습니다. 읽기 전용 컬렉션/목록을 포함하도록 EF9에서 확장되었습니다. 특히 EF9는 IReadOnlyList
, IReadOnlyCollection
또는 ReadOnlyCollection
형식의 컬렉션을 지원합니다. 예를 들어, 다음 코드에서 DaysVisited
는 규칙에 따라 기본 날짜 컬렉션으로 매핑됩니다.
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
읽기 전용 컬렉션은 원하는 경우 일반 변경 가능한 컬렉션으로 뒷받침될 수 있습니다. 예를 들어, 다음 코드에서 DaysVisited
는 기본 날짜 컬렉션으로 매핑되는 동시에 클래스의 코드가 기본 목록을 조작하도록 허용할 수 있습니다.
public class Pub
{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}
그런 다음 이러한 컬렉션을 일반적인 방법으로 쿼리에 사용할 수 있습니다. 예를 들어 다음 LINQ 쿼리는
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
이는 SQLite에서 다음 SQL로 변환됩니다.
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
키와 인덱스의 채우기 비율 지정
팁
여기에 표시된 코드는 ModelBuildingSample.cs에서 가져온 것입니다.
EF9는 EF Core 마이그레이션을 사용하여 키와 인덱스를 만들 때 SQL Server 채우기 비율 사양을 지원합니다. SQL Server 문서에서 "인덱스가 만들어지거나 다시 빌드될 때 채우기 비율 값은 데이터로 채워질 각 리프 수준 페이지의 공간 비율을 결정하고 각 페이지의 나머지 부분을 향후 성장을 위한 사용 가능한 공간으로 예약합니다. "
채우기 비율은 단일 또는 복합 기본 및 대체 키와 인덱스에 설정할 수 있습니다. 예시:
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
기존 테이블에 적용하면 테이블이 제약 조건에 대한 채우기 비율로 변경됩니다.
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
이 개선 사항은 @deano-hunter가 제공했습니다. 대단히 고맙습니다!
기존 모델 빌드 규칙을 더욱 확장할 수 있도록 만듭니다.
팁
여기에 표시된 코드는 CustomConventionsSample.cs에서 가져온 것입니다.
애플리케이션에 대한 공용 모델 빌드 규칙은 EF7에서 도입되었습니다. EF9에서는 기존 규칙 중 일부를 더 쉽게 확장할 수 있도록 만들었습니다. 예를 들어, EF7에서 특성별로 속성을 매핑하는 코드는 다음과 같습니다.
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;
}
}
}
}
EF9에서는 이를 다음과 같이 간소화할 수 있습니다.
public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
public이 아닌 생성자를 호출하도록 ApplyConfigurationsFromAssembly를 업데이트합니다.
이전 버전의 EF Core에서 ApplyConfigurationsFromAssembly
메서드는 매개 변수가 없는 공용 생성자를 사용하여 구문 형식만 인스턴스화했습니다. EF9에서는 실패할 때 생성되는 오류 메시지를 개선했으며 public이 아닌 생성자에 의한 인스턴스화도 사용하도록 설정했습니다. 이는 애플리케이션 코드로 인스턴스화해서는 안 되는 전용 중첩 클래스에 구성을 함께 배치할 때 유용합니다. 예시:
public class Country
{
public int Code { get; set; }
public required string Name { get; set; }
private class FooConfiguration : IEntityTypeConfiguration<Country>
{
private FooConfiguration()
{
}
public void Configure(EntityTypeBuilder<Country> builder)
{
builder.HasKey(e => e.Code);
}
}
}
또한 이 패턴이 엔터티 형식을 구성에 연결하기 때문에 혐오스럽다고 생각하는 사람들도 있으며, 구성이 엔터티 형식과 같은 위치에 있기 때문에 매우 유용하다고 생각하는 사람들도 있습니다. 여기서는 이에 대해 논쟁하지 않기로 합니다. :-)
SQL Server HierarchyId
팁
여기에 표시된 코드는 HierarchyIdSample.cs에서 가져온 것입니다.
HierarchyId 경로 생성을 위한 Sugar
SQL Server HierarchyId
형식에 대한 최고 수준의 지원이 EF8에 추가되었습니다. EF9에서는 트리 구조에서 새 자식 노드를 더 쉽게 만들 수 있도록 sugar 메서드가 추가되었습니다. 예를 들어, 다음 코드는 HierarchyId
속성이 있는 기존 엔터티를 쿼리합니다.
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
그러면 이 HierarchyId
속성을 사용하여 명시적인 문자열 조작 없이 자식 노드를 만들 수 있습니다. 예시:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
daisy
에 /4/1/3/1/
의 HierarchyId
가 있으면 child1
은 HierarchyId
"/4/1/3/1/1/"을 가져오고 child2
는 HierarchyId
"/4/1/3/1/2/"를 가져옵니다.
이 두 자식 사이에 노드를 만들려면 추가 자식 수준을 사용할 수 있습니다. 예시:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
그러면 노드 HierarchyId
/4/1/3/1/1.5/
가 만들어지고 사이에 배치 child1
됩니다 child2
.
이 개선 사항은 @Rezakazemi890이 제공했습니다. 대단히 고맙습니다!
도구
다시 빌드 횟수 감소
기본적으로 dotnet ef
명령줄 도구는 도구를 실행하기 전에 프로젝트를 빌드합니다. 도구를 실행하기 전에 다시 빌드하지 않는 것이 작동하지 않을 때 혼동을 일으키는 일반적인 원인이기 때문입니다. 숙련된 개발자는 --no-build
옵션을 사용하여 속도가 느려질 수 있는 이 빌드를 방지할 수 있습니다. 그러나 --no-build
옵션을 사용하더라도 다음에 EF 도구 외부에서 빌드할 때 프로젝트가 다시 빌드될 수 있습니다.
@Suchiman의 커뮤니티 기여로 이 문제가 해결되었다고 생각합니다. 그러나 MSBuild 동작에 대한 조정이 의도하지 않은 결과를 초래하는 경향이 있다는 사실도 알고 있으므로 여러분과 같은 분들에게 이 기능을 시도해보고 부정적인 환경이 있으면 다시 보고해 주시기를 요청하고 있습니다.
.NET