Udostępnij przez


Globalne filtry zapytań

Globalne filtry zapytań umożliwiają dołączanie filtru do typu jednostki i stosowanie tego filtru za każdym razem, gdy jest wykonywane zapytanie dotyczące tego typu jednostki; pomyśl o nich jako dodatkowy operator LINQ Where , który jest dodawany za każdym razem, gdy zostanie zapytany typ jednostki. Takie filtry są przydatne w różnych przypadkach.

Wskazówka

Przykład z tego artykułu można zobaczyć w witrynie GitHub.

Przykład podstawowy — miękkie usuwanie

W niektórych scenariuszach, zamiast usuwać wiersz z bazy danych, zaleca się zamiast tego ustawić flagę IsDeleted , aby oznaczyć wiersz jako usunięty. Ten wzorzec jest nazywany usuwaniem nietrwałym. Usuwanie nietrwałe umożliwia usunięcie wierszy w razie potrzeby lub zachowanie dziennika inspekcji, w którym usunięte wiersze są nadal dostępne. Globalne filtry zapytań mogą służyć do filtrowania miękko usuniętych wierszy domyślnie, jednocześnie umożliwiając dostęp do nich w określonych miejscach poprzez wyłączenie filtra dla konkretnego zapytania.

Aby włączyć miękkie usuwanie, dodajmy IsDeleted właściwość do naszego typu bloga:

public class Blog
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }

    public string Name { get; set; }
}

Teraz skonfigurujemy globalny filtr zapytań przy użyciu interfejsu HasQueryFilter API w programie OnModelCreating:

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);

Teraz możemy wykonywać zapytania dotyczące naszych Blog jednostek w zwykły sposób. Skonfigurowany filtr zapewni, że wszystkie zapytania będą domyślnie filtrować wszystkie wystąpienia, w których IsDeleted ma wartość true.

Należy pamiętać, że w tym momencie należy ręcznie ustawić IsDeleted w celu miękkiego usunięcia encji. W przypadku bardziej kompleksowego rozwiązania można zastąpić metodę typu SaveChangesAsync kontekstu, aby dodać logikę, która przechodzi przez wszystkie jednostki, które użytkownik usunął, i zmienia ich stan na zmodyfikowany, oznaczając właściwość IsDeleted jako true:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    ChangeTracker.DetectChanges();

    foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
    {
        item.State = EntityState.Modified;
        item.CurrentValues["IsDeleted"] = true;
    }

    return await base.SaveChangesAsync(cancellationToken);
}

Dzięki temu można używać interfejsów API EF, które usuwają wystąpienie jednostki w zwykły sposób i zamiast tego oznaczać je jako usunięte.

Korzystanie z danych kontekstowych — wielodostępność

Innym głównym scenariuszem dla globalnych filtrów zapytań jest wielodostępność, w której aplikacja przechowuje dane należące do różnych użytkowników w tej samej tabeli. W takich przypadkach zazwyczaj istnieje kolumna identyfikatora dzierżawy , która kojarzy wiersz z określoną dzierżawą, a filtry zapytań globalnych mogą służyć do automatycznego filtrowania wierszy bieżącej dzierżawy. Zapewnia to domyślnie silną izolację najemcy dla Twoich zapytań, eliminując potrzebę myślenia o filtrowaniu dla najemcy w każdym zapytaniu.

W przeciwieństwie do usuwania nietrwałego, wielodostępność wymaga znajomości bieżącego identyfikatora dzierżawy; ta wartość jest zwykle określana, np. gdy użytkownik uwierzytelnia się w Internecie. Dla celów EF identyfikator dzierżawy musi być dostępny w wystąpieniu kontekstu, aby globalny filtr zapytania mógł się do niego odwoływać i używać go podczas wykonywania zapytań. Zaakceptujmy tenantId parametr w konstruktorze naszego typu kontekstu i odwołajmy się do tego z naszego filtru:

public class MultitenancyContext(string tenantId) : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
    }
}

To wymaga utworzenia kontekstu, aby określić skojarzony identyfikator dzierżawy, i gwarantuje, że wyłącznie jednostki Blog z tym identyfikatorem są domyślnie zwracane z zapytań.

Uwaga / Notatka

W tym przykładzie pokazano tylko podstawowe pojęcia dotyczące multi-tenancy, które są potrzebne do zademonstrowania globalnych filtrów zapytań. Aby uzyskać więcej informacji na temat wielodostępu i platformy EF, zobacz Multi-tenancy in EF Core applications (Obsługa wielu dzierżaw w aplikacjach platformy EF Core).

Używanie wielu filtrów zapytań

Wywołanie HasQueryFilter przy użyciu prostego filtru zastępuje dowolny poprzedni filtr, więc w ten sposób nie można zdefiniować wielu filtrów dla tego samego typu jednostki:

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);

Uwaga / Notatka

Ta funkcja jest wprowadzana w programie EF Core 10.0 (w wersji zapoznawczej).

Aby zdefiniować wiele filtrów zapytań w tym samym typie jednostki, muszą być nazwane:

modelBuilder.Entity<Blog>()
    .HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
    .HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);

Dzięki temu można zarządzać poszczególnymi filtrami oddzielnie, w tym selektywnie wyłączać jeden, ale nie drugi.

Wyłączanie filtrów

Filtry mogą być wyłączone dla poszczególnych zapytań LINQ przy użyciu IgnoreQueryFilters operatora :

var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();

Jeśli skonfigurowano wiele nazwanych filtrów, spowoduje to wyłączenie wszystkich tych filtrów. Aby selektywnie wyłączyć określone filtry (począwszy od ef 10), przekaż listę nazw filtrów do wyłączenia:

var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();

Filtry zapytań i wymagane nawigacje

Ostrzeżenie

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.

Wymagane nawigacje w programie EF oznaczają, że powiązana jednostka jest zawsze obecna. Ponieważ sprzężenia wewnętrzne mogą służyć do pobierania powiązanych jednostek, jeśli wymagana jednostka powiązana jest filtrowana przez filtr zapytania, jednostka nadrzędna może również zostać odfiltrowana. Może to spowodować nieoczekiwane pobranie mniejszej liczby elementów niż oczekiwano.

Aby zilustrować problem, możemy użyć jednostek Blog i Post skonfigurować je w następujący sposób:

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() { Title = "Fish care 101" },
            new() { Title = "Caring for tropical fish" },
            new() { Title = "Types of ornamental fish" }
        ]
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts =
        [
            new() { Title = "Cat care 101" },
            new() { Title = "Caring for tropical cats" },
            new() { Title = "Types of ornamental cats" }
        ]
    });

Problem można zaobserwować podczas wykonywania następujących 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 wystąpień, ale drugie zapytanie zwraca tylko 3. Ta niezgodność występuje, ponieważ metoda Include w drugim zapytaniu ładuje powiązane obiekty Blog. 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]

Użycie INNER JOIN odfiltrowuje wszystkie Post wiersze, których powiązane Blog wiersze zostały odfiltrowane przez filtr zapytania. Ten problem można rozwiązać, konfigurując nawigację jako opcjonalną zamiast wymaganej nawigacji, co powoduje wygenerowanie LEFT JOIN przez EF zamiast elementu INNER JOIN.

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

Alternatywną metodą jest określenie spójnych filtrów zarówno dla typów jednostek Blog jak i Post. Po zastosowaniu pasujących filtrów do Blog i Post, wiersze Post, które mogłyby zakończyć się w nieoczekiwanym stanie, są usuwane, 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"));

Filtry zapytań i IEntityTypeConfiguration

Jeśli filtr zapytania musi uzyskać dostęp do identyfikatora dzierżawy lub podobnych informacji kontekstowych, IEntityTypeConfiguration<TEntity> może stanowić dodatkowe utrudnienie, ponieważ, w przeciwieństwie do OnModelCreating, nie ma łatwo dostępnego wystąpienia typu kontekstu do odwołania się z filtru zapytania. Aby obejść ten problem, dodaj fikcyjny kontekst do typu konfiguracji i odwołaj się do tego w następujący sposób:

private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
    private readonly SomeDbContext _context = null!;

    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
    }
}

Ograniczenia

Filtry zapytań globalnych mają następujące ograniczenia:

  • Filtry można zdefiniować tylko dla typu jednostki głównej hierarchii dziedziczenia.
  • 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ń.