전역 쿼리 필터는 메타데이터 모델의 엔터티 형식에 적용되는 LINQ 쿼리 조건자입니다(일반적으로 OnModelCreating
). 쿼리 조건자는 일반적으로 LINQ Where
쿼리 연산자에 전달되는 부울 식입니다. EF Core는 이러한 필터를 해당 엔터티 형식과 관련된 모든 LINQ 쿼리에 자동으로 적용합니다. EF Core는 Include 또는 탐색 속성을 사용하여 간접적으로 참조하는 엔터티 형식에도 적용합니다. 이 기능의 몇 가지 일반적인 애플리케이션은 다음과 같습니다.
-
일시 삭제 - 엔터티 형식이
IsDeleted
속성을 정의합니다. -
다중 테넌트 - 엔터티 형식이 속성을 정의합니다
TenantId
.
예시
다음 예제에서는 전역 쿼리 필터를 사용하여 간단한 블로깅 모델에서 다중 테넌트 및 일시 삭제 쿼리 동작을 구현하는 방법을 보여 줍니다.
팁 (조언)
GitHub에서 이 문서의 샘플을 볼 수 있습니다.
비고
여기서는 다중 테넌시를 간단한 예로 사용합니다. EF Core 애플리케이션에서 다중 테넌트를 위한 포괄적인 지침이 포함된 문서도 있습니다.
먼저 엔터티를 정의합니다.
public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public bool IsDeleted { get; set; }
public Blog Blog { get; set; }
}
_tenantId
엔터티의 Blog
필드 선언을 주목하세요. 이 필드는 각 블로그 인스턴스를 특정 테넌트에 연결하는 데 사용됩니다. 또한 IsDeleted
엔터티 형식에 Post
속성이 정의됩니다. 이 속성은 포스트 인스턴스가 "일시 삭제"되었는지 여부를 추적하는 데 사용됩니다. 즉, 인스턴스는 내부 데이터를 물리적으로 제거하지 않고 삭제된 것으로 표시됩니다.
다음으로, OnModelCreating
내의 쿼리 필터를 HasQueryFilter
API를 사용하여 구성합니다.
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
호출에 HasQueryFilter
전달된 조건자 식은 이제 해당 형식에 대한 모든 LINQ 쿼리에 자동으로 적용됩니다.
팁 (조언)
현재 테넌트를 설정하는 데 사용되는 DbContext 인스턴스 수준 필드를 _tenantId
사용합니다. 모델 수준 필터는 올바른 컨텍스트 인스턴스(즉, 쿼리를 실행하는 인스턴스)의 값을 사용합니다.
비고
현재 동일한 엔터티에서 여러 쿼리 필터를 정의할 수 없습니다. 마지막 쿼리 필터만 적용됩니다. 그러나 논리 AND
연산자(&&
C#)를 사용하여 여러 조건으로 단일 필터를 정의할 수 있습니다.
내비게이션 사용
전역 쿼리 필터를 정의하는 데 탐색을 사용할 수도 있습니다. 쿼리 필터에서 탐색을 사용하면 쿼리 필터가 재귀적으로 적용됩니다. EF Core가 쿼리 필터에 사용되는 탐색을 확장하면 참조된 엔터티에 정의된 쿼리 필터도 적용됩니다.
이를 설명하기 위해 다음과 같은 방법으로 쿼리 필터를 구성합니다 OnModelCreating
.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));
다음으로, 모든 Blog
엔터티를 쿼리합니다.
var filteredBlogs = await db.Blogs.ToListAsync();
이 쿼리는 다음 SQL을 생성하며, Blog
및 Post
엔터티에 대해 정의된 쿼리 필터를 모두 적용합니다.
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0
비고
현재 EF Core는 전역 쿼리 필터 정의에서 주기를 검색하지 않으므로 정의할 때 주의해야 합니다. 잘못 지정하면 쿼리 변환 중에 주기가 무한 루프로 이어질 수 있습니다.
필요한 탐색 경로를 통해 쿼리 필터로 엔터티에 액세스하기
주의
전역 쿼리 필터가 정의된 엔터티에 액세스하는 데 필요한 탐색을 사용하면 예기치 않은 결과가 발생할 수 있습니다.
필수 네비게이션에는 관련 엔터티가 항상 존재해야 합니다. 필요한 관련 엔터티가 쿼리 필터를 통해 필터링되면 부모 엔터티도 결과에 포함되지 않습니다. 따라서 결과에서 예상보다 적은 요소를 얻을 수 있습니다.
문제를 설명하기 위해 위에 지정된 Blog
및 Post
엔터티와 다음 OnModelCreating
메서드를 사용할 수 있습니다.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
모델은 다음 데이터로 시드할 수 있습니다.
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/fish",
Posts = new List<Post>
{
new Post { Title = "Fish care 101" },
new Post { Title = "Caring for tropical fish" },
new Post { Title = "Types of ornamental fish" }
}
});
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/cats",
Posts = new List<Post>
{
new Post { Title = "Cat care 101" },
new Post { Title = "Caring for tropical cats" },
new Post { Title = "Types of ornamental cats" }
}
});
두 개의 쿼리를 실행할 때 문제가 관찰될 수 있습니다.
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
위의 설정을 사용하면 첫 번째 쿼리는 6 Post
초만 반환하고 두 번째 쿼리는 3개만 반환합니다. 이 불일치는 두 번째 쿼리에서 Include
메서드가 관련 Blog
엔터티를 로드하기 때문에 발생합니다.
Blog
와 Post
사이의 탐색이 필요하므로, EF Core는 쿼리를 생성할 때 INNER JOIN
를 사용합니다.
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]
INNER JOIN
는 전역 쿼리 필터에 의해 관련 Post
가 제거된 모든 Blog
를 필터링합니다.
필요한 대신 선택적 탐색을 사용하여 해결할 수 있습니다.
이렇게 하면 첫 번째 쿼리는 이전과 동일하게 유지되지만 두 번째 쿼리는 이제 6개의 결과를 생성 LEFT JOIN
하고 반환합니다.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
대안적인 방법은 Blog
및 Post
엔터티 모두에 일관된 필터를 지정하는 것입니다.
이러한 방식으로 Blog
와 Post
둘 다에 일치하는 필터가 적용됩니다.
Post
예기치 않은 상태에 이를 수 있는 항목이 제거되며, 두 쿼리 모두 3개의 결과를 반환합니다.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
필터 비활성화
연산자를 사용하여 개별 LINQ 쿼리에 대해 필터를 사용하지 IgnoreQueryFilters 않도록 설정할 수 있습니다.
blogs = await db.Blogs
.Include(b => b.Posts)
.IgnoreQueryFilters()
.ToListAsync();
제한점
전역 쿼리 필터에는 다음과 같은 제한 사항이 있습니다.
- 상속 계층 구조의 루트 엔터티 형식에 대해서만 필터를 정의할 수 있습니다.
.NET