EF Core Azure Cosmos DB 공급자를 사용하여 쿼리
쿼리 기본 사항
EF Core LINQ 쿼리는 다른 데이터베이스 공급자와 동일한 방식으로 Azure Cosmos DB에 대해 실행될 수 있습니다. 예시:
public class Session
{
public Guid Id { get; set; }
public string Category { get; set; }
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
}
var stringResults = await context.Sessions
.Where(
e => e.Category.Length > 4
&& e.Category.Trim().ToLower() != "disabled"
&& e.Category.TrimStart().Substring(2, 2).Equals("xy", StringComparison.OrdinalIgnoreCase))
.ToListAsync();
참고 항목
Azure Cosmos DB 공급자는 다른 공급자와 동일한 LINQ 쿼리 집합을 변환하지 않습니다.
예를 들어 문서 간 쿼리는 데이터베이스에서 지원되지 않으므로 EF Include()
연산자는 Cosmos에서 지원되지 않습니다.
파티션 키
분할의 이점은 관련 데이터가 있는 파티션에 대해서만 쿼리를 실행하여 비용을 절감하고 더 빠른 결과 속도를 보장하는 것입니다. 파티션 키를 지정하지 않는 쿼리는 모든 파티션에서 실행되며 비용이 많이 들 수 있습니다.
EF 9.0부터 EF는 LINQ 쿼리의 Where
연산자에서 파티션 키 비교를 자동으로 검색하고 추출합니다. 계층적 파티션 키로 구성된 Session
엔터티 형식에 대해 다음 쿼리를 실행합니다.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Session>()
.HasPartitionKey(b => new { b.TenantId, b.UserId, b.SessionId })
}
var tenantId = "Microsoft";
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var username = "scott";
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId > 0
&& e.Username == username)
.ToListAsync();
EF에서 생성된 로그를 검사하면 이 쿼리가 다음과 같이 실행되는 것을 볼 수 있습니다.
Executed ReadNext (166.6985 ms, 2.8 RU) ActivityId='312da0d2-095c-4e73-afab-27072b5ad33c', Container='test', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE ((c["SessionId"] > 0) AND CONTAINS(c["Username"], "a"))
이러한 로그에서 다음을 확인할 수 있습니다.
- 처음 두 비교(
TenantId
및UserId
)가 해제되어WHERE
절이 아닌ReadNext
"파티션"에 표시됩니다. 즉, 쿼리가 해당 값의 하위 파티션에서만 실행됩니다. SessionId
또한 계층적 파티션 키의 일부이지만 같음 비교 대신 보다 큰 연산자(>
)를 사용하므로 해제할 수 없습니다. 일반 속성과 마찬가지로WHERE
절의 일부입니다.Username
은 파티션 키의 일부가 아닌 일반 속성이므로WHERE
절에도 남아 있습니다.
일부 파티션 키 값이 제공되지 않더라도 계층적 파티션 키를 사용하면 여전히 처음 두 속성에 해당하는 하위 파티션만 대상으로 지정할 수 있습니다. 이는 단일 파티션을 대상으로 지정하는 것만큼 효율적이지는 않지만(세 가지 속성 모두에 의해 식별됨) 모든 파티션을 대상으로 지정하는 것보다 훨씬 효율적입니다.
Where
연산자의 파티션 키 속성을 참조하는 대신 WithPartitionKey 연산자를 사용하여 명시적으로 지정할 수 있습니다.
var sessions = await context.Sessions
.WithPartitionKey(tenantId, userId)
.Where(e => e.SessionId > 0 && e.Username.Contains("a"))
.ToListAsync();
이는 위의 쿼리와 동일한 방식으로 실행되며, 쿼리에서 파티션 키를 보다 명시적으로 지정하려는 경우 더 바람직할 수 있습니다. 9.0 이전의 EF 버전에서는 WithPartitionKey를 사용해야 할 수 있습니다. 로그를 주시하여 쿼리가 예상대로 파티션 키를 사용하고 있는지 확인합니다.
지점 읽기
Azure Cosmos DB는 SQL을 통한 강력한 쿼리를 허용하지만 이러한 쿼리는 비용이 많이 들 수 있습니다. 또한 Cosmos DB는 id
속성과 전체 파티션 키를 모두 알 수 있을 때 사용할 수 있는 지점 읽기를 지원합니다. 이러한 지점 읽기는 특정 파티션의 특정 문서를 직접 식별하고 매우 효율적이며 비용을 절감하면서 실행할 수 있습니다. 가능한 한 포인트 읽기를 최대한 활용하는 방식으로 시스템을 설계하는 것이 좋습니다. 자세한 내용은 Cosmos DB 설명서를 참조하세요.
이전 섹션에서는 EF가 보다 효율적인 쿼리를 위해 Where
절에서 파티션 키 비교를 식별하고 추출하여 처리를 관련 파티션으로만 제한하는 것을 살펴보았습니다. 한 단계 더 나아가 쿼리에 id
속성도 제공할 수 있습니다. 다음 쿼리를 살펴보겠습니다.
var session = await context.Sessions.SingleAsync(
e => e.Id == someId
&& e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId);
이 쿼리에서는 Id
속성 값(Cosmos DB id
속성에 매핑됨)뿐만 아니라 모든 파티션 키 속성에 대한 값도 제공됩니다. 또한 쿼리에 대한 추가 구성 요소는 없습니다. 이러한 모든 조건이 충족되면 EF는 쿼리를 지점 읽기로 실행할 수 있습니다.
Executed ReadItem (46 ms, 1 RU) ActivityId='d7391311-2266-4811-ae2d-535904c42c43', Container='test', Id='9', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",10.0]'
쿼리가 효율적인 지점 읽기로 실행되었음을 나타내는 ReadItem
을 확인합니다. SQL 쿼리는 관련되어 있지 않습니다.
파티션 키 추출과 마찬가지로 EF 9.0에서 이 메커니즘을 크게 개선했습니다. 이전 버전은 지점 읽기를 안정적으로 검색하고 사용하지 않습니다.
페이지 매김
참고 항목
이 기능은 EF Core 9.0에서 도입되었으며 실험적입니다. 얼마나 유용했는지, 피드백이 있는 경우 알려주세요.
페이지 매김은 결과를 한 번에 검색하지 않고 페이지 단위로 검색하는 것을 말하며, 일반적으로 사용자 인터페이스가 표시되어 사용자가 결과의 페이지를 탐색할 수 있는 대규모 결과 집합에 대해 수행됩니다.
데이터베이스로 페이지 매김을 구현하는 일반적인 방법은 Skip
및 Take
LINQ 연산자(SQL의 OFFSET
및 LIMIT
)를 사용하는 것입니다. 페이지 크기의 결과가 10개인 경우 다음과 같이 세 번째 페이지를 EF Core로 가져올 수 있습니다.
var position = 20;
var nextPage = context.Session
.OrderBy(s => s.Id)
.Skip(position)
.Take(10)
.ToList();
아쉽게도 이 기술은 매우 비효율적이며 쿼리 비용을 상당히 증가시킬 수 있습니다. Cosmos DB는 연속 토큰을 사용하여 쿼리 결과를 페이지 매김하기 위한 특별한 메커니즘을 제공합니다.
CosmosPage firstPage = await context.Sessions
.OrderBy(s => s.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
string continuationToken = firstPage.ContinuationToken;
foreach (var session in firstPage.Values)
{
// Display/send the sessions to the user
}
LINQ 쿼리를 ToListAsync
로 종료하거나 이와 유사한 방식으로 종료하는 대신, 모든 페이지에서 최대 10개의 항목(데이터베이스에 항목이 더 적을 수 있음)을 가져오도록 지시하는 ToPageAsync
메서드를 사용합니다. 첫 번째 쿼리이므로 처음부터 결과를 가져오고 null
을 연속 토큰으로 전달하려고 합니다. ToPageAsync
는 연속 토큰과 페이지의 값(최대 10개 항목)을 노출하는 CosmosPage
를 반환합니다. 프로그램은 일반적으로 연속 토큰과 함께 해당 값을 클라이언트에 보냅니다. 이렇게 하면 나중에 쿼리를 다시 시작하고 더 많은 결과를 가져올 수 있습니다.
이제 사용자가 UI에서 "다음" 단추를 클릭하여 다음 10개 항목을 요청한다고 가정해 보겠습니다. 그러면 다음과 같이 쿼리를 실행할 수 있습니다.
CosmosPage nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
string continuationToken = nextPage.ContinuationToken;
foreach (var session in nextPage.Values)
{
// Display/send the sessions to the user
}
동일한 쿼리를 실행하지만 이번에는 첫 번째 실행에서 받은 연속 토큰을 전달합니다. 이렇게 하면 Cosmos DB에서 중단된 쿼리를 계속하고 다음 10개 항목을 가져오도록 지시합니다. 마지막 페이지를 가져오고 더 이상 결과가 없으면 연속 토큰은 null
이 되고, "다음" 단추가 회색으로 표시될 수 있습니다. 이 페이지 매김 방법은 Skip
및 Take
를 사용하는 것에 비해 매우 효율적이고 비용 효과적입니다.
Cosmos DB의 페이지 매김에 대한 자세한 내용은 이 페이지를 참조하세요.
참고 항목
Cosmos DB는 이전 페이지 매김을 지원하지 않으며 총 페이지 또는 항목 수를 제공하지 않습니다.
현재 ToPageAsync
는 Cosmos 특정이 아닌 보다 일반적인 EF 페이지 매김 API로 대체될 수 있으므로 실험적이라는 주석이 추가되었습니다. 현재 API를 사용하면 컴파일 경고(EF9102
)가 생성되지만 안전해야 합니다. 향후 변경 시 API 셰이프에 약간의 조정이 필요할 수 있습니다.
FindAsync
FindAsync
는 기본 키로 엔터티를 가져오고 엔터티가 이미 로드되고 컨텍스트에 의해 추적될 때 데이터베이스 왕복을 방지하는 데 유용한 API입니다.
관계형 데이터베이스에 익숙한 개발자는 Id
속성 등으로 구성된 엔터티 형식의 기본 키에 익숙합니다. EF Cosmos DB 공급자를 사용하는 경우 기본 키에는 JSON id
속성에 매핑된 속성 외에도 파티션 키 속성이 포함됩니다. Cosmos DB에서 서로 다른 파티션에 동일한 JSON id
속성이 있는 문서를 포함할 수 있으므로 결합된 id
와 파티션 키만이 컨테이너의 단일 문서를 고유하게 식별할 수 있습니다.
public class Session
{
public Guid Id { get; set; }
public string PartitionKey { get; set; }
...
}
var mySession = await context.FindAsync(id, pkey);
계층적 파티션 키가 있는 경우 모든 파티션 키 값을 구성된 순서대로 FindAsync
에 전달해야 합니다.
참고 항목
컨텍스트에서 엔터티를 이미 추적할 수 있고 데이터베이스 왕복을 방지하려는 경우에만 FindAsync
를 사용합니다.
그렇지 않으면 데이터베이스에서 엔터티를 로드해야 하는 경우 둘 사이에 성능 차이가 없는 SingleAsync
를 사용하면 됩니다.
SQL 쿼리
쿼리는 SQL로 직접 작성할 수도 있습니다. 예시:
var rating = 3;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Rating > {rating}")
.ToListAsync();
이 쿼리로 인해 다음 쿼리가 실행됩니다.
SELECT VALUE s
FROM (
SELECT VALUE c FROM root c WHERE c.Angle1 <= @p0
) s
FromSql
은 EF 9.0에 도입되었습니다. 이전 버전에서는 FromSqlRaw
를 대신 사용할 수 있지만, 해당 방법은 SQL 삽입 공격에 취약하다는 점에 유의하세요.
SQL 쿼리에 대한 자세한 내용은 SQL 쿼리에 대한 관계형 설명서를 참조하세요. 대부분의 콘텐츠는 Cosmos 공급자와도 관련이 있습니다.
함수 매핑
이 섹션에서는 Azure Cosmos DB 공급자로 쿼리할 때 어떤 .NET 메서드 및 멤버가 어떤 SQL 함수로 변환되는지 보여 줍니다.
날짜 및 시간 함수
.NET | SQL | 추가된 위치 |
---|---|---|
DateTime.UtcNow | GetCurrentDateTime() | |
DateTimeOffset.UtcNow | GetCurrentDateTime() | |
dateTime.Year1 | DateTimePart("yyyy", dateTime) | EF Core 9.0 |
dateTimeOffset.Year1 | DateTimePart("yyyy", dateTimeOffset) | EF Core 9.0 |
dateTime.AddYears(years)1 | DateTimeAdd("yyyy", dateTime) | EF Core 9.0 |
dateTimeOffset.AddYears(years)1 | DateTimeAdd("yyyy", dateTimeOffset) | EF Core 9.0 |
1 다른 구성 요소 멤버도 번역됩니다(월, 일...).
숫자 함수
.NET | SQL | 추가된 위치 |
---|---|---|
double.DegreesToRadians(x) | RADIANS(@x) | EF Core 8.0 |
double.RadiansToDegrees(x) | DEGREES(@x) | EF Core 8.0 |
EF.Functions.Random() | RAND() | |
Math.Abs(value) | ABS(@value) | |
Math.Acos(d) | ACOS(@d) | |
Math.Asin(d) | ASIN(@d) | |
Math.Atan(d) | ATAN(@d) | |
Math.Atan2(y, x) | ATN2(@y, @x) | |
Math.Ceiling(d) | CEILING(@d) | |
Math.Cos(d) | COS(@d) | |
Math.Exp(d) | EXP(@d) | |
Math.Floor(d) | FLOOR(@d) | |
Math.Log(a, newBase) | LOG(@a, @newBase) | |
Math.Log(d) | LOG(@d) | |
Math.Log10(d) | LOG10(@d) | |
Math.Pow(x, y) | POWER(@x, @y) | |
Math.Round(d) | ROUND(@d) | |
Math.Sign(value) | SIGN(@value) | |
Math.Sin(a) | SIN(@a) | |
Math.Sqrt(d) | SQRT(@d) | |
Math.Tan(a) | TAN(@a) | |
Math.Truncate(d) | TRUNC(@d) |
팁
여기에 나열된 메서드 외에도 해당 제네릭 수학 구현 및 MathF 메서드도 번역됩니다. 예를 들어 Math.Sin
, MathF.Sin
, double.Sin
, float.Sin
모두 SQL의 SIN
함수에 매핑합니다.
문자열 함수
.NET | SQL | 추가된 위치 |
---|---|---|
Regex.IsMatch(input, pattern) | RegexMatch(@pattern, @input) | EF Core 7.0 |
Regex.IsMatch(input, pattern, options) | RegexMatch(@input, @pattern, @options) | EF Core 7.0 |
string.Concat(str0, str1) | @str0 + @str1 | |
string.Equals(a, b, StringComparison.Ordinal) | STRINGEQUALS(@a, @b) | |
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) | STRINGEQUALS(@a, @b, true) | |
stringValue.Contains(value) | CONTAINS(@stringValue, @value) | |
stringValue.Contains(value, StringComparison.Ordinal) | CONTAINS(@stringValue, @value, false) | EF Core 9.0 |
stringValue.Contains(value, StringComparison.OrdinalIgnoreCase) | CONTAINS(@stringValue, @value, true) | EF Core 9.0 |
stringValue.EndsWith(value) | ENDSWITH(@stringValue, @value) | |
stringValue.EndsWith(value, StringComparison.Ordinal) | ENDSWITH(@stringValue, @value, false) | EF Core 9.0 |
stringValue.EndsWith(value, StringComparison.OrdinalIgnoreCase) | ENDSWITH(@stringValue, @value, true) | EF Core 9.0 |
stringValue.Equals(value, StringComparison.Ordinal) | STRINGEQUALS(@stringValue, @value) | |
stringValue.Equals(value, StringComparison.OrdinalIgnoreCase) | STRINGEQUALS(@stringValue, @value, true) | |
stringValue.FirstOrDefault() | LEFT(@stringValue, 1) | |
stringValue.IndexOf(value) | INDEX_OF(@stringValue, @value) | |
stringValue.IndexOf(value, startIndex) | INDEX_OF(@stringValue, @value, @startIndex) | |
stringValue.LastOrDefault() | RIGHT(@stringValue, 1) | |
stringValue.Length | LENGTH(@stringValue) | |
stringValue.Replace(oldValue, newValue) | REPLACE(@stringValue, @oldValue, @newValue) | |
stringValue.StartsWith(value) | STARTSWITH(@stringValue, @value) | |
stringValue.StartsWith(value, StringComparison.Ordinal) | STARTSWITH(@stringValue, @value, false) | EF Core 9.0 |
stringValue.StartsWith(value, StringComparison.OrdinalIgnoreCase) | STARTSWITH(@stringValue, @value, true) | EF Core 9.0 |
stringValue.Substring(startIndex) | SUBSTRING(@stringValue, @startIndex, LENGTH(@stringValue)) | |
stringValue.Substring(startIndex, length) | SUBSTRING(@stringValue, @startIndex, @length) | |
stringValue.ToLower() | LOWER(@stringValue) | |
stringValue.ToUpper() | UPPER(@stringValue) | |
stringValue.Trim() | TRIM(@stringValue) | |
stringValue.TrimEnd() | RTRIM(@stringValue) | |
stringValue.TrimStart() | LTRIM(@stringValue) |
기타 함수
.NET | SQL | 주의 |
---|---|---|
collection.Contains(item) | @item IN @collection | |
EF.Functions.CoalesceUndefined(x, y)1 | x ?? y | EF Core 5.0에서 추가됨 |
EF.Functions.IsDefined(x) | IS_DEFINED(x) | EF Core 5.0에서 추가됨 |
EF.Functions.VectorDistance(vector1, vector2)2 | VectorDistance(vector1, vector2) | EF Core 9.0에서 추가됨, 실험적 |
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 | VectorDistance(vector1, vector2, bruteForce) | EF Core 9.0에서 추가됨, 실험적 |
EF.Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 | VectorDistance(vector1, vector2, bruteForce, distanceFunction) | EF Core 9.0에서 추가됨, 실험적 |
1 EF.Functions.CoalesceUndefined
는 null
이 아닌 undefined
를 병합합니다. null
을 병합하려면 일반 C# ??
연산자를 사용합니다.
2 Azure Cosmos DB에서 벡터 검색을 사용하는 방법에 대한 자세한 내용은 설명서를 참조하세요. Cosmos DB 벡터 검색은 실험적이며 API는 변경될 수 있습니다.
.NET