Filtry zapytań globalnych
Globalne filtry zapytań to predykaty zapytań LINQ stosowane do typów jednostek w modelu metadanych (zwykle w OnModelCreating
systemie ). Predykat zapytania jest wyrażeniem logicznym zwykle przekazywanym do operatora zapytania LINQ Where
. Program EF Core automatycznie stosuje takie filtry do wszystkich zapytań LINQ obejmujących te typy jednostek. Program EF Core stosuje je również do typów jednostek, do których odwołuje się pośrednio, za pomocą właściwości Dołączanie lub nawigacja. Oto niektóre typowe zastosowania tej funkcji:
- Usuwanie nietrwałe — typ jednostki definiuje
IsDeleted
właściwość. - Wielodostępność — typ jednostki definiuje
TenantId
właściwość.
Przykład
W poniższym przykładzie pokazano, jak używać globalnych filtrów zapytań do implementowania zachowań zapytań obejmujących wiele dzierżaw i usuwania nietrwałego w prostym modelu blogowania.
Napiwek
Przykład z tego artykułu można zobaczyć w witrynie GitHub.
Uwaga
Wielodostępność jest używana tutaj jako prosty przykład. Istnieje również artykuł z kompleksowymi wskazówkami dotyczącymi wielodostępności w aplikacjach EF Core.
Najpierw zdefiniuj jednostki:
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; }
}
Zanotuj deklarację _tenantId
pola w jednostce Blog
. To pole będzie używane do skojarzenia każdego wystąpienia blogu z określoną dzierżawą. Zdefiniowana również właściwość jest właściwością IsDeleted
Post
typu jednostki. Ta właściwość służy do śledzenia, czy wystąpienie post zostało "usunięte nietrwale". Oznacza to, że wystąpienie jest oznaczone jako usunięte bez fizycznego usuwania bazowych danych.
Następnie skonfiguruj filtry zapytań przy OnModelCreating
użyciu interfejsu HasQueryFilter
API.
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
Wyrażenia predykatu przekazane do wywołań będą teraz automatycznie stosowane do HasQueryFilter
wszystkich zapytań LINQ dla tych typów.
Napiwek
Zwróć uwagę na użycie pola poziomu wystąpienia DbContext: _tenantId
służy do ustawiania bieżącej dzierżawy. Filtry na poziomie modelu będą używać wartości z poprawnego wystąpienia kontekstu (czyli wystąpienia, które wykonuje zapytanie).
Uwaga
Obecnie nie można zdefiniować wielu filtrów zapytań w tej samej jednostce — zostanie zastosowany tylko ostatni. Można jednak zdefiniować jeden filtr z wieloma warunkami przy użyciu operatora logicznego AND
(&&
w języku C#).
Korzystanie z nawigacji
Możesz również użyć nawigacji podczas definiowania globalnych filtrów zapytań. Użycie nawigacji w filtrze zapytań spowoduje, że filtry zapytań będą stosowane rekursywnie. Gdy program EF Core rozszerza nawigacje używane w filtrach zapytań, będzie również stosować filtry zapytań zdefiniowane dla odwołanych jednostek.
Aby zilustrować te filtry zapytań OnModelCreating
w następujący sposób:
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"));
Następnie wykonaj zapytanie dotyczące wszystkich Blog
jednostek:
var filteredBlogs = await db.Blogs.ToListAsync();
To zapytanie tworzy następujący kod SQL, który stosuje filtry zapytań zdefiniowane dla jednostek Blog
i 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
Uwaga
Obecnie program EF Core nie wykrywa cykli w globalnych definicjach filtrów zapytań, dlatego należy zachować ostrożność podczas ich definiowania. Jeśli określono niepoprawnie, cykle mogą prowadzić do nieskończonych pętli podczas tłumaczenia zapytań.
Uzyskiwanie dostępu do jednostki z filtrem zapytań przy użyciu wymaganej nawigacji
Uwaga
Użycie wymaganej nawigacji w celu uzyskania dostępu do jednostki, która ma zdefiniowany globalny filtr zapytań, może prowadzić do nieoczekiwanych wyników.
Wymagana nawigacja oczekuje, że powiązana jednostka będzie zawsze obecna. W razie potrzeby powiązana jednostka jest filtrowana według filtru zapytania, jednostka nadrzędna nie byłaby w wyniku. W związku z tym możesz uzyskać mniej elementów niż oczekiwano w wyniku.
Aby zilustrować problem, możemy użyć jednostek Blog
i Post
określonych powyżej i następującej OnModelCreating
metody:
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Model może zostać wstępnie wypełniony następującymi danymi:
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" }
}
});
Problem można zaobserwować podczas wykonywania dwóch zapytań:
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
W przypadku powyższej konfiguracji pierwsze zapytanie zwraca wszystkie 6 Post
s, jednak drugie zapytanie zwraca tylko 3. Ta niezgodność występuje, ponieważ Include
metoda w drugim zapytaniu ładuje powiązane Blog
jednostki. Ponieważ nawigacja między elementami Blog
i Post
jest wymagana, program EF Core używa INNER JOIN
podczas konstruowania zapytania:
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
Użyj filtrów dla wszystkich Post
elementów, których powiązane Blog
zostały usunięte przez globalny filtr zapytań.
Można go rozwiązać za pomocą opcjonalnej nawigacji zamiast wymaganej.
Dzięki temu pierwsze zapytanie pozostaje takie samo jak wcześniej, jednak drugie zapytanie będzie teraz generować LEFT JOIN
i zwracać 6 wyników.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Alternatywne podejście polega na określeniu spójnych filtrów dla jednostek Blog
i Post
.
W ten sposób pasujące filtry są stosowane do elementów Blog
i Post
. Post
s, które mogą skończyć się nieoczekiwanym stanem, zostaną usunięte, a oba zapytania zwracają 3 wyniki.
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"));
Wyłączanie filtrów
Filtry mogą być wyłączone dla poszczególnych zapytań LINQ przy użyciu IgnoreQueryFilters operatora .
blogs = await db.Blogs
.Include(b => b.Posts)
.IgnoreQueryFilters()
.ToListAsync();
Ograniczenia
Filtry zapytań globalnych mają następujące ograniczenia:
- Filtry można zdefiniować tylko dla głównego typu jednostki hierarchii dziedziczenia.