Accélération du noyau Entity Framework

Ne soyez pas avide!


Lors de la sélection des données, vous devez en choisir exactement autant que vous le souhaitez à la fois. Ne récupérez jamais toutes les données d'une table!

Faux:

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

Correctement:

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();


Faux:

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

Correctement:

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

La validation des données intégrée peut être effectuée lorsqu'une requête renvoie certains enregistrements.

Faux:


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;
}

Correctement:

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();

Wow, wow, wow, cadencé.

Il est temps de rafraîchir un peu vos connaissances sur les techniques LINQ.

Examinons les différences entre ToList AsEnumerable AsQueryable


Alors 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 .
  • Utilisez AsQueryable si vous souhaitez une requête de base de données qui peut être améliorée avant d'être exécutée côté serveur.


Un exemple d'utilisation d'AsQueryable dans le cas le plus simple:

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;
        }


La magie de la lecture simple


Si vous n'avez pas besoin de modifier les données, affichez simplement la méthode .AsNoTracking () .

Échantillonnage lent

var blogs = context.Blogs.ToList();

Récupération rapide (lecture seule)

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

Vous sentez que vous vous êtes déjà un peu réchauffé?

Types de chargement des données liées


Pour ceux qui ont oublié ce qu'est le chargement paresseux .

Le chargement différé ( chargement différé) signifie que les données associées sont chargées de manière transparente à partir de la base de données lors de l'accès à la propriété de navigation. En savoir plus ici .

Et en même temps, permettez-moi de vous rappeler d'autres types de chargement de données liées.

La charge active (chargement avide) signifie que les données associées sont chargées à partir de la base de données dans le cadre de la demande initiale.

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();
}

Attention! À partir de la version EF Core 3.0.0, chaque Include entraînera l'ajout d'un JOIN supplémentaire aux requêtes SQL créées par les fournisseurs relationnels, tandis que les versions précédentes généraient des requêtes SQL supplémentaires. Cela peut modifier considérablement les performances de vos requêtes, pour le meilleur ou pour le pire. En particulier, les requêtes LINQ avec un très grand nombre d'instructions d'inclusion peuvent être divisées en plusieurs requêtes LINQ distinctes.

Le chargement explicite ( chargement explicite) signifie que les données associées seront explicitement chargées ultérieurement à partir de la base de données.

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();
}

Jerk et percée! Vous continuez?

Prêt à accélérer encore plus?


Pour accélérer considérablement lors de la récupération de données d'une structure complexe et même anormales d'une base de données relationnelle, il existe deux façons de procéder: utiliser des vues indexées (1) ou, mieux encore, des données pré-préparées (calculées) sous une forme plate simple pour l'affichage (2).

(1) Vue indexée dans le contexte de MS SQL Server

La vue indexée possède un index cluster unique. Un index cluster unique est stocké dans SQL Server et est mis à jour comme tout autre index cluster. Une vue indexée est plus importante que les vues standard, qui incluent le traitement complexe d'un grand nombre de lignes, par exemple, l'agrégation d'une grande quantité de données ou la combinaison de plusieurs lignes.

Si ces vues sont souvent référencées dans les requêtes, nous pouvons améliorer les performances en créant un index cluster unique pour la vue. Pour une vue standard, l'ensemble de résultats n'est pas stocké dans la base de données; à la place, l'ensemble de résultats est calculé pour chaque requête, mais dans le cas d'un index clusterisé, l'ensemble de résultats est stocké dans la base de données de la même manière qu'une table avec un index clusterisé. Les requêtes qui n'utilisent pas spécifiquement la vue indexée peuvent même bénéficier de l'existence d'un index cluster de la vue.

La présentation d'un indice a un certain coût en termes de performances. Si nous créons une vue indexée, chaque fois que nous modifions les données dans les tables de base, SQL Server doit prendre en charge non seulement les enregistrements d'index dans ces tables, mais également les enregistrements d'index dans la vue. Dans les éditions SQL Server pour les développeurs et les entreprises, l'optimiseur peut utiliser des index de vue pour optimiser les requêtes qui ne spécifient pas de vue indexée. Toutefois, dans d'autres éditions de SQL Server, la requête doit inclure une vue indexée et fournir une indication NOEXPAND pour tirer parti de l'index dans la vue.

(2) Si vous avez besoin de faire une demande qui nécessite l'affichage de plus de trois niveaux de tables connexes d'un montant de trois ou plus avec un CRUD accrucharge, la meilleure façon serait de calculer périodiquement l'ensemble de résultats, de l'enregistrer dans un tableau et de l'utiliser pour l'affichage. La table résultante dans laquelle les données seront stockées doit avoir une clé primaire et des index sur les champs de recherche dans LINQ .

Et l'asynchronie?


Oui! Nous l'utilisons autant que possible! Voici un exemple:

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();
}

Et oui, avez-vous oublié quelque chose pour augmenter la productivité? Buum!

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


Remarque: la méthode Do () a été ajoutée à des fins de démonstration uniquement, afin d'indiquer l'opérabilité de la méthode GetFeralDistrictsAsync () . Comme mes collègues l'ont correctement noté, un autre exemple d'asynchronie pure est nécessaire.

Et permettez-moi de le donner sur la base du concept d'un composant de vue dans 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



Permettez-moi de vous rappeler lorsque des requêtes sont exécutées dans Entity Framework Core.

Lorsque vous appelez des instructions LINQ, vous créez simplement une vue de requête en mémoire. La demande n'est envoyée à la base de données qu'après le traitement des résultats.

Voici les opérations les plus courantes qui entraînent l'envoi d'une demande à la base de données.

  • Itérer sur les résultats dans une boucle for.
  • À l'aide d'un opérateur, tel que ToList, ToArray, Single, Count.
  • Liaison des données des résultats de requête à l'interface utilisateur.

Comment organiser le code EF Core en termes d'architecture d'application?


(1) Du point de vue de l'architecture de l'application, vous devez vous assurer que le code d'accès à votre base de données est isolé / séparé dans un endroit clairement défini (de manière isolée). Cela vous permet de trouver du code de base de données qui affecte les performances.

(2) Ne mélangez pas le code d'accès de votre base de données avec d'autres parties de l'application, telles que l'interface utilisateur ou l'API. Ainsi, le code d'accès à la base de données peut être modifié sans se soucier d'autres problèmes non liés à la base de données.

Comment enregistrer les données correctement et rapidement en utilisant SaveChanges ?


Si les enregistrements insérés sont identiques, il est judicieux d'utiliser une seule opération de sauvegarde pour tous les enregistrements.

Faux

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();
}
}

Correctement

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();
}
}

Il y a toujours une exception à la règle. Si le contexte de transaction est complexe, c'est-à-dire qu'il comprend plusieurs opérations indépendantes, vous pouvez enregistrer après chaque opération. Et il est encore plus correct d'utiliser le stockage asynchrone dans une transaction.

//    
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();
        }
    }
}

Déclencheurs, champs calculés, fonctions personnalisées et EF Core


Pour réduire la charge sur les applications contenant EF Core, il est logique d'utiliser des champs calculés simples et des déclencheurs de base de données, mais il vaut mieux ne pas s'impliquer, car l'application peut être très déroutante. Mais les fonctions définies par l'utilisateur peuvent être très utiles, en particulier pendant les opérations de récupération!

Concurrence dans EF Core


Si vous souhaitez tout paralléliser pour accélérer, alors interrompez: EF Core ne prend pas en charge l'exécution de plusieurs opérations parallèles dans une seule instance du contexte. Attendez la fin d'une opération avant de commencer la suivante. Pour ce faire, vous devez généralement spécifier le mot clé wait dans chaque opération asynchrone.

EF Core utilise des requêtes asynchrones pour éviter de bloquer le flux lors de l'exécution d'une requête dans la base de données. Les demandes asynchrones sont importantes pour garantir une réponse rapide de l'interface utilisateur dans les clients lourds. Ils peuvent également augmenter le débit dans une application Web, où vous pouvez libérer le thread pour gérer d'autres demandes. Voici un exemple:

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

Que savez-vous des requêtes compilées LINQ?


Si vous avez une application qui exécute à plusieurs reprises des requêtes structurellement similaires dans Entity Framework, vous pouvez souvent améliorer les performances en compilant la requête une fois et en l'exécutant plusieurs fois avec des paramètres différents. Par exemple, une application peut avoir besoin d'obtenir tous les clients dans une ville spécifique; la ville est indiquée lors de l'exécution par l'utilisateur dans le formulaire. LINQ to Entities prend en charge l'utilisation de requêtes compilées à cette fin.

À partir de .NET Framework 4.5, les requêtes LINQ sont mises en cache automatiquement. Cependant, vous pouvez toujours utiliser des requêtes LINQ compilées pour réduire ce coût dans les exécutions suivantes, et les requêtes compilées peuvent être plus efficaces que les requêtes LINQ qui sont automatiquement mises en cache. Notez que les requêtes LINQ to Entities qui appliquent l'opérateur Enumerable.Contains aux collections en mémoire ne sont pas mises en cache automatiquement. De plus, le paramétrage des collections en mémoire dans les requêtes LINQ compilées n'est pas autorisé.

De nombreux exemples peuvent être trouvés ici .

Ne créez pas de grands contextes DbContext!


En général, je connais beaucoup d'entre vous, si ce n'est presque tous, des f_u__c_k__e_r__s paresseux et toute la base de données que vous placez dans un seul contexte, en particulier cela est typique de l'approche Database-First. Et en vain vous le faites! Voici un exemple de la façon dont le contexte peut être divisé. Bien sûr, les tables de connexion entre les contextes devront être dupliquées, c'est un inconvénient. D'une manière ou d'une autre, si vous avez plus de 50 tableaux dans le contexte, il vaut mieux penser à le diviser.

Utilisation du regroupement de contextes (regroupement de DdContext)


La signification du pool DbContext est de permettre la réutilisation des instances DbContext à partir du pool, ce qui dans certains cas peut conduire à de meilleures performances que la création d'une nouvelle instance à chaque fois. C'est également la principale raison de la création d'un pool de connexions dans ADO.NET, bien que les gains de performances pour les connexions soient plus importants car les connexions sont généralement une ressource plus difficile.

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();
        }

Comment éviter les erreurs inutiles avec CRUD dans EF Core?


N'insérez jamais de calculs dans le même code. Séparez toujours la formation / préparation d'un objet et son insertion / mise à jour. Il suffit de le répartir par fonction: vérifier les données saisies par l'utilisateur, calculer les données préliminaires nécessaires, cartographier ou créer un objet, et l'opération CRUD réelle.

Que faire lorsque les performances des applications sont vraiment mauvaises?


La bière n'aidera certainement pas ici. Mais ce qui va aider, c'est la séparation de la lecture et de l'écriture dans l'architecture de l'application, suivie de l'allocation de ces opérations sur les sockets. Pensez à utiliser le modèle CQRS (Command and Query Responsibility Segregation) et essayez également de diviser les tables en insert et en lecture entre deux bases de données.

Accélérez les applications pour vous, amis et collègues!

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


All Articles