Blog Dla Młodszych Programistów C#/.NET

wtorek, 10 sierpnia 2021
Entity Framework Core ma ogromne możliwości. W ostatnim artykule wspominałem Ci o tym, że może generować szybkie i optymalne zapytania na bazie danych, aby to jednak robił, warto poznać i stosować się przynajmniej do kilku dobrych praktyk. 5 najważniejszych przedstawię Ci w tym artykule na konkretnych przykładach.

5 Najlepszych Praktyk z Entity Framework Core


Omówienie projektu


Będziemy dzisiaj również pracować na projekcie z poprzedniego materiału (Wprowadzenie Do Entity Framework Core), to znaczy Bookstore. W tym projekcie są już zainstalowane wymagane pakiet dla Entity Framework Core.

Kontekst:
namespace Bookstore
{
    class ApplicationDbContext : DbContext
    {
        public DbSet<Book> Books { get; set; }
        public DbSet<Category> Categories { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var builder = new ConfigurationBuilder()
                .AddJsonFile($"appsettings.json", true, true);

            var config = builder.Build();

            optionsBuilder
                .UseSqlServer(config["ConnectionString"]);
        }
    }
}

Encje:
namespace Bookstore
{
    public class Book
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }

        public int? CategoryId { get; set; }
        public Category Category { get; set; }
    }
}
namespace Bookstore
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Book> Books { get; set; } = new HashSet<Book>();
    }
}

Klasa główna aplikacji konsolowej:
namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
        }
    }
}


1. Sprawdzanie wygenerowanych przez Entity Framework Core zapytań


Dobrą praktyką jest sprawdzanie wygenerowanych przez Entity Framework Core zapytań. Można to zrobić na kilka sposobów. Najprościej, możesz wyświetlać te zapytania w konsoli. Wystarczy w konfiguracji, czyli w naszym przypadku w klasie ApplicationDbContext wywołać 2 dodatkowy metody. Po pierwsze, metodę LogTo, gdzie jako parametry przekazujemy Console.WriteLine, a także informacje o danych, które chcemy logować.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var builder = new ConfigurationBuilder()
        .AddJsonFile($"appsettings.json", true, true);

    var config = builder.Build();

    optionsBuilder
        .LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information)
        .UseSqlServer(config["ConnectionString"]);
}

A także, drugą metodę EnableSensitiveDataLogging(), dzięki której będą również wyświetlane wszystkie szczegółowe, bardziej wrażliwe dane, między innymi wartości parametrów itp.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var builder = new ConfigurationBuilder()
        .AddJsonFile($"appsettings.json", true, true);

    var config = builder.Build();

    optionsBuilder
        .LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information)
        .EnableSensitiveDataLogging()
        .UseSqlServer(config["ConnectionString"]);
}

Teraz po wywołaniu zapytania na bazie danych za pomocą Entity Framework Core, zostanie ono wyświetlone w naszej konsoli.

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var books = new List<Book>();

            using (var context = new ApplicationDbContext())
            {
                books = await context.Books.ToListAsync();
            }

            foreach (var item in books)
            {
                Console.WriteLine($"Id: {item.Id}. Książka: '{item.Name}' - {item.Price:0.00} PLN.");
            }
        }
    }
}
Entity Framework Core dobre praktyki - logowanie konsola

Oprócz tego możesz te zapytania podglądać w SQL Server Profiler, a nawet możesz je zapisywać do pliku. W tej aplikacji konsolowej mam już zainstalowany framework do logowania danych NLog i możemy teraz łatwo skonfigurować logowanie wszystkich zapytań do pliku przez Entity Framework Core. Jeżeli nie wiesz jak zainstalować NLog, to odsyłam Cię do tego artykułu NLog, gdzie pokazywałem krok po kroku jak to zrobić. Także na początek w klasie, w której definiujemy konfigurację, potrzebuje zainicjalizować fabrykę logger i następnie przekazać ją w konfiguracji jako parametr do metody UseLoggerFactory(). Całość będzie wyglądać w ten sposób:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using System;

namespace Bookstore
{
    class ApplicationDbContext : DbContext
    {
        public static readonly ILoggerFactory _loggerFactory = new NLogLoggerFactory();

        public DbSet<Book> Books { get; set; }
        public DbSet<Category> Categories { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var builder = new ConfigurationBuilder()
                .AddJsonFile($"appsettings.json", true, true);

            var config = builder.Build();

            optionsBuilder
                .UseLoggerFactory(_loggerFactory)
                .LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information)
                .EnableSensitiveDataLogging()
                .UseSqlServer(config["ConnectionString"]);
        }
    }
}

Jeżeli teraz uruchomisz aplikację, to w pliku powinien pojawić się mniej więcej taki zapis:

Entity Framework Core dobre praktyki - logowanie nlog plik

Także w zależności od tego, co potrzebujesz, możesz wybrać dowolną opcję. Warto sobie dodać takie zapisywanie zapytań do swojej aplikacji.


2. Monitorowanie długości wykonywania zapytań


Drugą dobrą praktyką jest monitorowanie również długości wykonywania zapytań. Możesz dodać takie monitorowanie tylko do pojedynczych zapytań, ale jeszcze lepiej zrobić sobie jakiegoś wrappera, który będzie w ten sposób monitorował wszystkie zapytania w Twojej aplikacji. W celu zweryfikowania długości zapytania skorzystamy z instancji klasy Stopwatch:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();
                books = await context.Books.ToListAsync();
                stopwatch.Stop();
            }

            foreach (var item in books)
            {
                Console.WriteLine($"Id: {item.Id}. Książka: '{item.Name}' - {item.Price:0.00} PLN.");
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}

Najpierw inicjalizujemy nowy obiekt stopwatch. Następnie bezpośrednio przed zapytaniem wywołujemy metodę Start() na tym obiekcie, a po wykonaniu zapytania metodę Stop(). Dzięki temu możemy obliczyć czas wykonania zapytania i wyświetlić go np. w Milisekundach poniżej.

Entity Framework Core dobre praktyki - monitorowanie czasu zapytania konsola


3. Blokowanie śledzenia obiektów w zapytaniach tylko do odczytu


Trzecią dobrą praktyką jest blokowanie śledzenia obiektów (czyli change tracking) dla zapytań tylko do odczytu. Jeżeli wykonujemy dowolne zapytanie na bazie danych w Entity Framework Core, to domyślnie pobrane obiekty są śledzone. Możemy to wyłączyć globalnie dla wszystkich zapytań, ale lepszą praktyką jest blokowanie tego śledzenia dla zapytań read only. Aby to zrobić, wystarczy wywołać metodę AsNoTracking() na naszym zapytaniu.

books = await context.Books
    .AsNoTracking()
    .ToListAsync();

Dzięki temu nasze zapytanie przede wszystkim będzie dużo szybsze. W szczególności można to zauważyć na większych zapytaniach, gdzie jest więcej danych. Także, jeżeli wiesz, że dane zapytanie jest tylko do odczytu. To znaczy, tylko chcesz wyświetlić użytkownikowi jakieś dane do przeglądu, nie będziesz na nich robił żadnych zmian, to najlepszą praktyką jest nieblokowanie śledzenia obiektów.


4. Filtrowanie danych po stronie serwera


Czwarta dobra praktyka to filtrowanie danych po stronie serwera. Czasem spotykam się z zapytaniami, które najpierw pobierają wszystkie rekordy z bazy danych, a dopiero później są filtrowane w pamięci. Zobacz na ten przykład:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();

                books = await context.Books
                    .AsNoTracking()
                    .ToListAsync();

                books = books
                    .Where(x => x.Name == "Książka 1")
                    .ToList();

                stopwatch.Stop();
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}
Entity Framework Core dobre praktyki - filtrowanie po stronie klienta

Czyli najpierw pobieramy sobie wszystkie dane, a następnie filtrujemy je po nazwie. Takie filtrowanie jak najbardziej zadziała, ale tak jak wspomniałem, z bazy danych zostaną pobrane najpierw wszystkie rekordy, a dopiero później zostaną one dodatkowo przefiltrowane w pamięci. Oczywiście takie zapytanie będzie dużo wolniejsze od tego, gdy przeprowadzimy takie filtrowanie od razu na bazie danych, a nie na kolekcji w pamięci.

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();

                books = await context.Books
                    .AsNoTracking()
                    .Where(x => x.Name == "Książka 1")
                    .ToListAsync();

                stopwatch.Stop();
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}

Jeżeli zapytanie wywołamy w ten sposób, to na bazie danych zostanie wywołane jedno zapytanie, które od razu będzie filtrowało wszystkie rekordy już po stronie serwera. To zapytanie będzie wtedy dużo szybsze (również jeszcze bardziej to będzie zauważalne przy większej ilości danych). Tak będzie wyglądać zapytanie:

Entity Framework Core dobre praktyki - filtrowanie po stronie serwera


5. Minimalizowanie wysyłanych komend do bazy danych


Piątą dobrą praktyką, o której chciałbym wspomnieć, to minimalizowanie wysyłanych komend do bazy danych. Szczególnie dotyczy to dodawania wiele rekordów w pętli za pomocą metody Add() lub za pomocą AddRange(), obie wersje są złe. Zobacz na ten przykład:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            for (int i = 0; i < 10000; i++)
            {
                books.Add(new Book { Name = $"Książka {i}", Price = 120 });
            }

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();

                foreach (var item in books)
                {
                    context.Books.Add(item);
                }

                await context.SaveChangesAsync();

                stopwatch.Stop();
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}
Entity Framework Core dobre praktyki - minimalizowanie komend add

Jak możesz zauważyć, na bazie danych zostaje wykonanych mnóstwo zapytań, te dane są dodawane partiami, także trwa to bardzo długo i nie jest zbyt optymalne.

Możesz również skorzystać z metody AddRange, ale jak się za chwilę przekonasz, nie jest to wcale dużo lepsze rozwiązanie.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            for (int i = 0; i < 10000; i++)
            {
                books.Add(new Book { Name = $"Książka {i}", Price = 120 });
            }

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();

                context.Books.AddRange(books);

                await context.SaveChangesAsync();

                stopwatch.Stop();
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}
Entity Framework Core dobre praktyki - minimalizowanie komend AddRange

Jak widzisz, zapytanie jest już trochę szybsze, ale tylko trochę. Dalej te dane są przekazywane partiami, co prawda w trochę bardziej optymalny sposób, ale mnóstwo komend jest wywoływanych na bazie danych. Zarówno metoda Add oraz AddRange nie jest optymalna.

W celu dodania wielu rekordów, najlepiej skorzystać z jednego z wielu dostępnych darmowych rozszerzeń do Entity Framework Core, na przykład takiego o nazwie EFCore.BulkExtensions. Tutaj jest dostępna cała dokumentacja: EFCore.BulkExtensions . Wystarczy tylko zainstalować pakiet przez nugeta: EFCore.BulkExtensions.

Entity Framework Core dobre praktyki - minimalizowanie komend efcore bulk extensions

Następnie możesz wywołać komendę w ten sposób:

using EFCore.BulkExtensions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Bookstore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var stopwatch = new Stopwatch();

            var books = new List<Book>();

            for (int i = 0; i < 10000; i++)
            {
                books.Add(new Book { Name = $"Książka {i}", Price = 120 });
            }

            using (var context = new ApplicationDbContext())
            {
                stopwatch.Start();

                await context.BulkInsertAsync(books);

                stopwatch.Stop();
            }

            Console.WriteLine($"Długość wykonania zapytania: {stopwatch.ElapsedMilliseconds} MS.");
        }
    }
}
Entity Framework Core dobre praktyki - minimalizowanie komend efcore bulkextensions add

Jak widzisz, zapytanie jest dużo szybsze, całe zapytanie jest bardziej optymalne i nie wykonuje oddzielne tylu komend. Dzięki temu pakietowi również możemy w podobny sposób aktualizować i usuwać wiele danych w Entity Framework Core.


PODSUMOWANIE


Przedstawiłem Ci w tym materiale 5 moim zdaniem najlepszych praktyk przy pracy z Entity Framework Core. Pamiętaj, żeby:
  • Sprawdzać wygenerowane zapytania.
  • Monitorować długość wykonywania zapytań.
  • Zablokować śledzenie obiektów dla zapytań tylko do odczytu.
  • Filtrować dane już po stronie serwera.
  • Minimalizować wysyłanie komend do bazy danych
Jeżeli będziesz pamiętał o tych zasadach, to z całą pewnością Twoje zapytania będę szybsze i nie będziesz miał problemów z Entity Framework Core.

Jeżeli taki artykuł Ci się spodobał, to koniecznie dołącz do mojej społeczności. Zapisz się na darmowy newsletter, gdzie co tydzień dzielę się wartościowymi materiałami w szczególności dotyczącymi C# i platformy .NET (darmowy zapis – newsletter).

Poprzedni artykuł - Wprowadzenie Do Entity Framework Core – Współpraca z Bazą Danych w C#.
Następny artykuł - 5 Najczęstszych Błędów w Entity Framework Core.
Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Dodaj komentarz

Wyszukiwarka

© Copyright 2024 modestprogrammer.pl. Wszelkie prawa zastrzeżone. Regulamin. Polityka prywatności. Design by Kazimierz Szpin