Acelerando o Entity Framework Core

Não seja ganancioso!


Ao selecionar dados, você precisa escolher exatamente quantos você precisa por vez. Nunca recupere todos os dados de uma tabela!

Errado:

using var ctx = new EFCoreTestContext(optionsBuilder.Options);                
//    ID  ,       !
ctx.FederalDistricts.Select(x=> new { x.ID, x.Name, x.ShortName }).ToList();

Corretamente:

using var ctx = new EFCoreTestContext(optionsBuilder.Options);  
//     ID     !
ctx.FederalDistricts.Select(x=> new { x.Name, x.ShortName }).ToList();
ctx.FederalDistricts.Select(x => new MyClass { Name = x.Name, ShortName = x.ShortName }).ToList();


Errado:

var blogs = context.Blog.ToList(); //       . ?
//     ?
var somePost = blogs.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));

Corretamente:

var somePost = context.Blog.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));

A validação de dados integrada pode ser realizada quando uma consulta retorna alguns registros.

Errado:


var blogs = context.Blogs.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList();

public static string StandardizeUrl(string url)
{
    url = url.ToLower();
    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }
    return url;
}

Corretamente:

var blogs = context.Blogs.AsEnumerable().Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList();
 
//  
var blogs = context.Blogs.Where(blog => blog.Contains("dotnet"))
    .OrderByDescending(blog => blog.Rating)
    .Select(blog => new
    {
        Id = blog.BlogId,
        Url = StandardizeUrl(blog.Url)
    })
    .ToList();

Uau, uau, uau!

É hora de atualizar um pouco o seu conhecimento das técnicas de LINQ.

Vejamos as diferenças entre ToList AsEnumerable AsQueryable


Então, ToList

  • .
  • .ToList() (lazy loading), .

AsEnumerable

  • (lazy loading)
  • : Func <TSource, bool>
  • ( Where/Take/Skip , , select * from Table1,
  • , N )
  • : Linq-to-SQL + Linq-to-Object.
  • IEnumerable (lazy loading).

AsQueryable

  • (lazy loading)
  • :
    AsQueryable(IEnumerable)  AsQueryable<TElement>(IEnumerable<TElement>) 

  • Expression T-SQL ( ), .
  • DbSet ( Entity Framework) AsQueryable .
  • , Take(5) «select top 5 * SQL» . , SQL , . AsQueryable() , AsEnumerable() T-SQL Linq .
  • Use AsQueryable se desejar uma consulta ao banco de dados que possa ser aprimorada antes de executar no lado do servidor.


Um exemplo de uso do AsQueryable no caso mais simples:

public IEnumerable<EmailView> GetEmails(out int totalRecords, Guid? deviceWorkGroupID,
                DateTime? timeStart, DateTime? timeEnd, string search, int? confirmStateID, int? stateTypeID, int? limitOffset, int? limitRowCount, string orderBy, bool desc)
        {
            var r = new List<EmailView>();

            using (var db = new GJobEntities())
            {
                var query = db.Emails.AsQueryable();

                if (timeStart != null && timeEnd != null)
                {
                    query = query.Where(p => p.Created >= timeStart && p.Created <= timeEnd);
                }

                if (stateTypeID != null && stateTypeID > -1)
                {
                    query = query.Where(p => p.EmailStates.OrderByDescending(x => x.AtTime).FirstOrDefault().EmailStateTypeID == stateTypeID);
                }


                if (confirmStateID != null && confirmStateID > -1)
                {
                    var boolValue = confirmStateID == 1 ? true : false;
                    query = query.Where(p => p.IsConfirmed == boolValue);
                }

                if (!string.IsNullOrEmpty(search))
                {
                    search = search.ToLower();
                    query = query.Where(p => (p.Subject + " " + p.CopiesEmails + " " + p.ToEmails + " " + p.FromEmail + " " + p.Body)
                                        .ToLower().Contains(search));
                }

                if (deviceWorkGroupID != Guid.Empty)
                {
                    query = query.Where(x => x.SCEmails.FirstOrDefault().SupportCall.Device.DeviceWorkGroupDevices.FirstOrDefault(p => p.DeviceWorkGroupID == deviceWorkGroupID) != null);
                }

                totalRecords = query.Count();
                query = query.OrderByDescending(p => p.Created);
                if (limitOffset.HasValue)
                {
                    query = query.Skip(limitOffset.Value).Take(limitRowCount.Value);
                }
                var items = query.ToList(); //    

                foreach (var item in items)
                {
                    var n = new EmailView
                    {
                        ID = item.ID,
                        SentTime = item.SentTime,
                        IsConfirmed = item.IsConfirmed,
                        Number = item.Number,
                        Subject = item.Subject,
                        IsDeleted = item.IsDeleted,
                        ToEmails = item.ToEmails,
                        Created = item.Created,
                        CopiesEmails = item.CopiesEmails,
                        FromEmail = item.FromEmail,
                    };

                    //     - 

                    r.Add(n);
                }
            }

            return r;
        }


A magia da leitura simples


Se você não precisar alterar dados, basta exibir o método .AsNoTracking () .

Amostragem lenta

var blogs = context.Blogs.ToList();

Busca rápida (somente leitura)

var blogs = context.Blogs.AsNoTracking().ToList();

Sente que você já se aqueceu um pouco?

Tipos de carregamento de dados relacionados


Para aqueles que esqueceram o que é carregamento preguiçoso .

Carregamento lento ( carregamento lento) significa que os dados associados são carregados de forma transparente a partir do banco de dados ao acessar a propriedade de navegação. Leia mais aqui .

E, ao mesmo tempo, deixe-me lembrá-lo de outros tipos de carregamento de dados relacionados.

Carga ativa (carregamento ansioso) significa que os dados associados são carregados do banco de dados como parte da solicitação inicial.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
                .ThenInclude(author => author.Photo)
        .Include(blog => blog.Owner)
            .ThenInclude(owner => owner.Photo)
        .ToList();
}

Atenção! A partir da versão EF Core 3.0.0, cada Include fará com que um JOIN adicional seja adicionado às consultas SQL geradas pelos provedores relacionais, enquanto as versões anteriores geraram consultas SQL adicionais. Isso pode alterar significativamente o desempenho de suas consultas, para melhor ou para pior. Em particular, as consultas LINQ com um número extremamente grande de instruções de inclusão podem ser divididas em várias consultas LINQ separadas.

Carregamento explícito ( carregamento explícito) significa que os dados associados foram carregados explicitamente no banco de dados posteriormente.

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var goodPosts = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Where(p => p.Rating > 3)
        .ToList();
}

Idiota e avanço! Se movendo?

Pronto para acelerar ainda mais?


Para acelerar drasticamente ao buscar dados complexamente estruturados e até anormais de um banco de dados relacional, há duas maneiras de fazer isso: use vistas indexadas (1) ou, melhor ainda, dados pré-preparados (calculados) de forma simples e plana para exibição (2).

(1) Visualização indexada no contexto do MS SQL Server

A exibição indexada possui um índice clusterizado exclusivo. Um índice clusterizado exclusivo é armazenado no SQL Server e é atualizado como qualquer outro índice clusterizado. Uma exibição indexada é mais significativa que a exibição padrão, que inclui processamento complexo de um grande número de linhas, por exemplo, agregando uma grande quantidade de dados ou combinando várias linhas.

Se tais visualizações são frequentemente referenciadas em consultas, podemos melhorar o desempenho criando um índice em cluster exclusivo para a visualização. Para a visualização padrão, o conjunto de resultados não é armazenado no banco de dados; em vez disso, o conjunto de resultados é calculado para cada consulta, mas, no caso de um índice em cluster, o conjunto de resultados é armazenado no banco de dados da mesma maneira que uma tabela com um índice em cluster. As consultas que não usam especificamente a exibição indexada podem até se beneficiar da existência de um índice clusterizado da exibição.

A representação de um índice tem um certo custo na forma de produtividade. Se criarmos uma exibição indexada, sempre que alterarmos os dados nas tabelas base, o SQL Server deverá suportar não apenas os registros de índice nessas tabelas, mas também os registros de índice na exibição. Nas edições do SQL Server para desenvolvedores e empresas, o otimizador pode usar índices de exibição para otimizar consultas que não especificam uma exibição indexada. No entanto, em outras edições do SQL Server, a consulta deve incluir uma exibição indexada e fornecer uma dica NOEXPAND para aproveitar o índice na exibição.

(2) Se você precisar fazer uma solicitação que exija a exibição de mais de três níveis de tabelas relacionadas no valor de três ou mais com CRUD aumentadocarga, a melhor maneira seria calcular periodicamente o conjunto de resultados, salvá-lo em uma tabela e usar para exibição. A tabela resultante na qual os dados serão armazenados deve ter uma Chave Primária e índices nos campos de pesquisa no LINQ .

E a assincronia?


Sim! Nós o usamos sempre que possível! Aqui está um exemplo:

public void Do()
{
    var myTask = GetFederalDistrictsAsync ();
    foreach (var item in myTask.Result)
    {
         // 
    }
}

public async Task<List<FederalDistrict>> GetFederalDistrictsAsync()
{
    var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
    optionsBuilder.UseSqlServer(conn);
    using var context = new EFCoreTestContext(optionsBuilder.Options);
    return await context.FederalDistricts.ToListAsync();
}

E sim, você esqueceu algo para aumentar a produtividade? Buum!

return await context.FederalDistricts.<b>AsNoTracking()</b>.ToListAsync();


Nota: o método Do () foi adicionado apenas para fins de demonstração, a fim de indicar a operabilidade do método GetFederalDistrictsAsync () . Como meus colegas observaram corretamente, é necessário outro exemplo de pura assincronia.

E deixe-me dar isso com base no conceito de um componente de exibição no ASP .NET Core :

//  
public class PopularPosts : ViewComponent
    {
        private readonly IStatsRepository _statsRepository;

        public PopularPosts(IStatsRepository statsRepository)
        {
            _statsRepository = statsRepository;
        }

        public async Task<IViewComponentResult> InvokeAsync()
        {
           //         -
            var federalDistricts = await _statsRepository.GetFederalDistrictsAsync(); 
            var model = new TablePageModel()
            {
                FederalDistricts = federalDistricts,
            };

            return View(model);
        }
    }
    // 
    
    /// <summary>
    ///  -   .... -
    /// </summary>
    public interface IStatsRepository
    {
        /// <summary>
        ///        
        /// </summary>
        /// <returns></returns>
        IEnumerable<FederalDistrict> FederalDistricts();

        /// <summary>
        ///        
	/// !!!
        /// </summary>
        /// <returns></returns>
        Task<List<FederalDistrict>> GetFederalDistrictsAsync();
    }	
	
    /// <summary>
    /// -   .... -
    /// </summary>
    public class StatsRepository : IStatsRepository
    {
        private readonly DbContextOptionsBuilder<EFCoreTestContext>
            optionsBuilder = new DbContextOptionsBuilder<EFCoreTestContext>();
        private readonly IConfigurationRoot configurationRoot;

        public StatsRepository()
        {
            IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
                    .SetBasePath(Environment.CurrentDirectory)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
            configurationRoot = configurationBuilder.Build();
        }

        public async Task<List<FederalDistrict>> GetFederalDistrictsAsync()
        {
            var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
            optionsBuilder.UseSqlServer(conn);
            using var context = new EFCoreTestContext(optionsBuilder.Options);
            return await context.FederalDistricts.Include(x => x.FederalSubjects).ToListAsync();
        }

        public IEnumerable<FederalDistrict> FederalDistricts()
        {
            var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
            optionsBuilder.UseSqlServer(conn);

            using var ctx = new EFCoreTestContext(optionsBuilder.Options);
            return ctx.FederalDistricts.Include(x => x.FederalSubjects).ToList();
        }
    }

   //         Home\Index 
    <div id="tableContainer">
            @await Component.InvokeAsync("PopularPosts")
     </div>
  //   HTML      Shared\Components\PopularPosts\Default.cshtml



Deixe-me lembrá-lo quando as consultas são executadas no Entity Framework Core.

Ao chamar instruções LINQ, você simplesmente cria uma exibição de consulta na memória. A solicitação é enviada ao banco de dados somente após o processamento dos resultados.

A seguir, são apresentadas as operações mais comuns que resultam no envio de uma solicitação ao banco de dados.

  • Repita os resultados em um loop for.
  • Usando um operador, como ToList, ToArray, Single, Count.
  • Vinculando dados dos resultados da consulta à interface do usuário.

Como organizar o código EF Core em termos de arquitetura de aplicativos?


(1) Do ponto de vista da arquitetura do aplicativo, você precisa garantir que o código de acesso ao seu banco de dados seja isolado / separado em um local claramente definido (isolado). Isso permite encontrar o código do banco de dados que afeta o desempenho.

(2) Não misture o código de acesso do seu banco de dados com outras partes do aplicativo, como a interface do usuário ou a API. Assim, o código de acesso ao banco de dados pode ser alterado sem se preocupar com outros problemas não relacionados ao banco de dados.

Como salvar dados corretamente e rapidamente usando SaveChanges ?


Se os registros inseridos forem iguais, faz sentido usar uma operação de salvamento para todos os registros.

Errado

using(var db = new NorthwindEntities())
{
var transaction = db.Database.BeginTransaction();

try
{
    //   1
    var  obj1 = new Customer();
    obj1.CustomerID = "ABCDE";
    obj1.CompanyName = "Company 1";
    obj1.Country = "USA";
    db.Customers.Add(obj1);

  //          db.SaveChanges();

    //   2
    var  obj2 = new Customer();
    obj2.CustomerID = "PQRST";
    obj2.CompanyName = "Company 2";    
    obj2.Country = "USA";
    db.Customers.Add(obj2);

    //   
    db.SaveChanges();

    transaction.Commit();
}
catch
{
    transaction.Rollback();
}
}

Corretamente

using(var db = new NorthwindEntities())
{
var transaction = db.Database.BeginTransaction();

try
{
   //  1
    var  obj1 = new Customer();
    obj1.CustomerID = "ABCDE";
    obj1.CompanyName = "Company 1";
    obj1.Country = "USA";
    db.Customers.Add(obj1); 

    //   2
    var  obj2 = new Customer();
    obj2.CustomerID = "PQRST";
    obj2.CompanyName = "Company 2";    
    obj2.Country = "USA";
    db.Customers.Add(obj2);

   //    N 
    db.SaveChanges();

    transaction.Commit();
}
catch
{
    transaction.Rollback();
}
}

Sempre existem exceções à regra. Se o contexto da transação for complexo, isto é, consistir em várias operações independentes, você poderá salvar após cada operação. E é ainda mais correto usar armazenamento assíncrono em uma transação.

//    
public async Task<IActionResult> AddDepositToHousehold(int householdId, DepositRequestModel model)
{
    using (var transaction = await Context.Database.BeginTransactionAsync(IsolationLevel.Snapshot))
    {
        try
        {
            //     
            var deposit = this.Mapper.Map<Deposit>(model);
            await this.Context.Deposits.AddAsync(deposit);

            await this.Context.SaveChangesAsync();

            //    
               var debtsToPay = await this.Context.Debts.Where(d => d.HouseholdId == householdId && !d.IsPaid).OrderBy(d => d.DateMade).ToListAsync();

            debtsToPay.ForEach(d => d.IsPaid = true);

            await this.Context.SaveChangesAsync();

            //   
            var household = this.Context.Households.FirstOrDefaultAsync(h => h.Id == householdId);

            household.Balance += model.DepositAmount;

            await this.Context.SaveChangesAsync();

            transaction.Commit();
            return this.Ok();
        }
        catch
        {
            transaction.Rollback();
            return this.BadRequest();
        }
    }
}

Gatilhos, campos computados, funções personalizadas e EF Core


Para reduzir a carga nos aplicativos que contêm EF Core, faz sentido usar campos calculados simples e gatilhos de banco de dados, mas é melhor não se envolver, pois o aplicativo pode ser muito confuso. Mas funções definidas pelo usuário podem ser muito úteis, especialmente durante operações de busca!

Concorrência no EF Core


Se você deseja paralelizar tudo para acelerar, interrompa: O EF Core não suporta a execução de várias operações paralelas em uma instância do contexto. Aguarde a conclusão de uma operação antes de iniciar a próxima. Para fazer isso, você geralmente precisa especificar a palavra-chave wait em cada operação assíncrona.

O EF Core usa consultas assíncronas para evitar o bloqueio do fluxo ao executar uma consulta no banco de dados. Solicitações assíncronas são importantes para garantir uma resposta rápida da interface do usuário em clientes espessos. Eles também podem aumentar a taxa de transferência em um aplicativo Web, onde você pode liberar o encadeamento para lidar com outras solicitações. Aqui está um exemplo:

public async Task<List<Blog>> GetBlogsAsync()
{
    using (var context = new BloggingContext())
    {
        return await context.Blogs.ToListAsync();
    }
}

O que você sabe sobre as consultas compiladas pelo LINQ?


Se você tiver um aplicativo que executa repetidamente consultas estruturalmente semelhantes no Entity Framework, geralmente poderá melhorar o desempenho compilando a consulta uma vez e executando-a várias vezes com parâmetros diferentes. Por exemplo, um aplicativo pode precisar obter todos os clientes em uma cidade específica; a cidade é indicada em tempo de execução pelo usuário no formulário. O LINQ to Entities suporta o uso de consultas compiladas para esse fim.

Começando com o .NET Framework 4.5, as consultas LINQ são armazenadas em cache automaticamente. No entanto, você ainda pode usar consultas LINQ compiladas para reduzir esse custo em execuções subseqüentes, e as consultas compiladas podem ser mais eficientes do que as consultas LINQ que são armazenadas em cache automaticamente. Observe que as consultas LINQ to Entities que aplicam o operador Enumerable.Contains às coleções na memória não são armazenadas em cache automaticamente. Além disso, a parametrização de coleções na memória em consultas LINQ compiladas não é permitida.

Muitos exemplos podem ser encontrados aqui .

Não crie contextos DbContext grandes!


Em geral, eu conheço muitos de vocês, se não quase todos, de preguiçosos f_u__c_k__e_r__s e de todo o banco de dados que você coloca em um contexto, especialmente isso é típico da abordagem Database-First. E em vão você faz isso! A seguir, é apresentado um exemplo de como o contexto pode ser dividido. Obviamente, as tabelas de conexão entre os contextos terão que ser duplicadas, isso é um sinal de menos. De uma forma ou de outra, se você tiver mais de 50 tabelas no contexto, é melhor pensar em dividi-la.

Usando o agrupamento de contexto (agrupando o DdContext)


O significado do pool DbContext é permitir a reutilização de instâncias DbContext do pool, o que em alguns casos pode levar a um desempenho melhor do que a criação de uma nova instância a cada vez. Esse também é o principal motivo para a criação de um pool de conexões no ADO.NET, embora os ganhos de desempenho nas conexões sejam mais significativos, pois as conexões geralmente são um recurso mais difícil.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Demos
{
    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class BloggingContext : DbContext
    {
        public static long InstanceCount;

        public BloggingContext(DbContextOptions options)
            : base(options)
            => Interlocked.Increment(ref InstanceCount);

        public DbSet<Blog> Blogs { get; set; }
    }

    public class BlogController
    {
        private readonly BloggingContext _context;

        public BlogController(BloggingContext context) => _context = context;

        public async Task ActionAsync() => await _context.Blogs.FirstAsync();
    }

    public class Startup
    {
        private const string ConnectionString
            = @"Server=(localdb)\mssqllocaldb;Database=Demo.ContextPooling;Integrated Security=True;ConnectRetryCount=0";

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<BloggingContext>(c => c.UseSqlServer(ConnectionString));
        }
    }

    public class Program
    {
        private const int Threads = 32;
        private const int Seconds = 10;

        private static long _requestsProcessed;

        private static async Task Main()
        {
            var serviceCollection = new ServiceCollection();
            new Startup().ConfigureServices(serviceCollection);
            var serviceProvider = serviceCollection.BuildServiceProvider();

            SetupDatabase(serviceProvider);

            var stopwatch = new Stopwatch();

            MonitorResults(TimeSpan.FromSeconds(Seconds), stopwatch);

            await Task.WhenAll(
                Enumerable
                    .Range(0, Threads)
                    .Select(_ => SimulateRequestsAsync(serviceProvider, stopwatch)));
        }

        private static void SetupDatabase(IServiceProvider serviceProvider)
        {
            using (var serviceScope = serviceProvider.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<BloggingContext>();

                if (context.Database.EnsureCreated())
                {
                    context.Blogs.Add(new Blog { Name = "The Dog Blog", Url = "http://sample.com/dogs" });
                    context.Blogs.Add(new Blog { Name = "The Cat Blog", Url = "http://sample.com/cats" });
                    context.SaveChanges();
                }
            }
        }
        private static async Task SimulateRequestsAsync(IServiceProvider serviceProvider, Stopwatch stopwatch)
        {
            while (stopwatch.IsRunning)
            {
                using (var serviceScope = serviceProvider.CreateScope())
                {
                    await new BlogController(serviceScope.ServiceProvider.GetService<BloggingContext>()).ActionAsync();
                }

                Interlocked.Increment(ref _requestsProcessed);
            }
        }

        private static async void MonitorResults(TimeSpan duration, Stopwatch stopwatch)
        {
            var lastInstanceCount = 0L;
            var lastRequestCount = 0L;
            var lastElapsed = TimeSpan.Zero;

            stopwatch.Start();

            while (stopwatch.Elapsed < duration)
            {
                await Task.Delay(TimeSpan.FromSeconds(1));

                var instanceCount = BloggingContext.InstanceCount;
                var requestCount = _requestsProcessed;
                var elapsed = stopwatch.Elapsed;
                var currentElapsed = elapsed - lastElapsed;
                var currentRequests = requestCount - lastRequestCount;

                Console.WriteLine(
                    $"[{DateTime.Now:HH:mm:ss.fff}] "
                    + $"Context creations/second: {instanceCount - lastInstanceCount} | "
                    + $"Requests/second: {Math.Round(currentRequests / currentElapsed.TotalSeconds)}");

                lastInstanceCount = instanceCount;
                lastRequestCount = requestCount;
                lastElapsed = elapsed;
            }

            Console.WriteLine();
            Console.WriteLine($"Total context creations: {BloggingContext.InstanceCount}");
            Console.WriteLine(
                $"Requests per second:     {Math.Round(_requestsProcessed / stopwatch.Elapsed.TotalSeconds)}");

            stopwatch.Stop();
        }

Como evitar erros desnecessários com CRUD no EF Core?


Nunca insira cálculos no mesmo código. Sempre separe a formação / preparação de um objeto e sua inserção / atualização. Apenas espalhe-o por função: verificando os dados inseridos pelo usuário, calculando os dados preliminares necessários, mapeando ou criando um objeto e a operação CRUD real.

O que fazer quando as coisas estão realmente ruins com o desempenho do aplicativo?


Cerveja definitivamente não vai ajudar aqui. Mas o que ajudará é a separação da leitura e gravação na arquitetura do aplicativo, seguida pela alocação dessas operações nos soquetes. Pense em usar o padrão CQRS (Command and Query Responsibility Segregation) e tente dividir as tabelas em inserção e leitura entre dois bancos de dados.

Acelere as aplicações para você, amigos e colegas!

Source: https://habr.com/ru/post/undefined/


All Articles