Не будь жадиной!
При выборке данных выбирать нужно ровно столько сколько нужно за один раз. Никогда не извлекайте все данные из таблицы!
Неправильно:
using var ctx = new EFCoreTestContext(optionsBuilder.Options);
// Мы возвращаем колонку ID с сервера, но никогда не используем и это неправильно!
ctx.FederalDistricts.Select(x=> new { x.ID, x.Name, x.ShortName }).ToList();
Правильно:
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();
Неправильно:
var blogs = context.Blog.ToList(); // Тут вы скопировали ВСЮ таблицу в память. Зачем?
// Чтобы выбрать лишь некоторые записи?
var somePost = blogs.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));
Правильно:
var somePost = context.Blog.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));
Встроенная проверка данных может быть выполнена, когда запрос вернул какие-то записи.
Неправильно:
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;
}
Правильно:
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();
Вау, вау, вау, разогнался.
Самое время немного освежить знания по методам LINQ.
Давайте рассмотрим отличия между ToList AsEnumerable AsQueryable
Итак, 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 определённые вами.
- Используйте AsQueryable если хотите запрос к базе данных который может быть улучшен перед запуском на стороне сервера.
Пример использования AsQueryable в простейшеем случае:
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;
}
Волшебство простого чтения
Если вам не нужно менять данные, только отобразить используйте .AsNoTracking() метод.
Медленная выборка
var blogs = context.Blogs.ToList();
Быстрая выборка (только на чтение)
var blogs = context.Blogs.AsNoTracking().ToList();
Чувствую, вы немного уже размялись?
Типы загрузки связанных данных
Для тех, кто забыл, что такое lazy loading.
Ленивая загрузка (Lazy loading) означает, что связанные данные прозрачно загружаются из базы данных при обращении к свойству навигации. Подробнее читаем тут .
И заодно, напомню о других типах загрузки связанных данных.
Активная загрузка (Eager loading) означает, что связанные данные загружаются из базы данных как часть первоначального запроса.
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();
}
Внимание! Начиная с версии EF Core 3.0.0, каждое Include будет вызывать добавление дополнительного JOIN к запросам SQL, создаваемым реляционными поставщиками, тогда как предыдущие версии генерировали дополнительные запросы SQL. Это может значительно изменить производительность ваших запросов, в лучшую или в худшую сторону. В частности, запросы LINQ с чрезвычайно большим числом операторов включения могут быть разбиты на несколько отдельных запросов LINQ.
Явная загрузка (Explicit loading) означает, что связанные данные явно загружаются из базы данных позднее.
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();
}
Рывок и прорыв! Двигаемся дальше?
Готовы ускориться еще больше?
Чтобы резко ускориться при выборке сложно структурированных и даже ненормализованных данных из реляционной базы данных есть два способа сделать это: используйте индексированные представления (1) или что еще лучше – предварительно подготовленные(вычисленные) данные в простой плоской форме для отображения (2).
(1) Индексированное представление в контексте MS SQL Server
Индексированное представление имеет уникальный кластеризованный индекс. Уникальный кластерный индекс хранится в SQL Server и обновляется, как и любой другой кластерный индекс. Индексированное представление является более значительным по сравнению со стандартными представлениями, которые включают сложную обработку большого количества строк, например, агрегирование большого количества данных или объединение множества строк.
Если на такие представления часто ссылаются в запросах, мы можем повысить производительность, создав уникальный кластеризованный индекс для представления. Для стандартного представления набор результатов не сохраняется в базе данных, вместо этого набор результатов вычисляется для каждого запроса, но в случае кластеризованного индекса набор результатов сохраняется в базе данных точно так же, как таблица с кластеризованным индексом. Запросы, которые специально не используют индексированное представление, могут даже выиграть от существования кластеризованного индекса из представления.
Представление индекса имеет определенную стоимость в виде производительности. Если мы создаем индексированное представление, каждый раз, когда мы изменяем данные в базовых таблицах, SQL Server должен поддерживать не только записи индекса в этих таблицах, но также и записи индекса в представлении. В редакциях SQL Server для разработчиков и предприятий оптимизатор может использовать индексы представлений для оптимизации запросов, которые не указывают индексированное представление. Однако в других выпусках SQL Server запрос должен включать индексированное представление и указывать подсказку NOEXPAND, чтобы получить преимущество от индекса в представлении.
(2) Если нужно сделать запрос, требующий отображения более трех уровней связанных таблиц в количестве три и более c повышенной CRUD нагрузкой, лучшим способом будет задуматься о том, чтобы периодически вычислять результирующий набор, сохранять его в таблице и использовать для отображения. Результирующая таблица, в которой будут сохраняться данные должна иметь Primary Key и индексы по полям поиска в LINQ.
Что насчет асинхронности?
Да! Используем ее где только можно! Вот пример:
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();
}
И да, ничего не забыли для повышения производительности? Бууум!
return await context.FederalDistricts.<b>AsNoTracking()</b>.ToListAsync();
Внимание: метод Do() добавлен для демонстрационных целей только, с целью указать работоспособность метода GetFederalDistrictsAsync(). Как правильно заметили мои коллеги тутнужен другой пример чистой асинхронности.
И давайте я его приведу на основе понятия компонента представления в 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
Напомню, когда выполняются запросы в Entity Framework Core.
При вызове операторов LINQ вы просто создаете представление запроса в памяти. Запрос отправляется в базу данных только после обработки результатов.
Ниже приведены наиболее распространенные операции, которые приводят к отправке запроса в базу данных.
- Итерация результатов в цикле for.
- Использование оператора, например ToList, ToArray, Single, Count.
- Привязка данных результатов запроса к пользовательскому интерфейсу.
Как же организовать код EF Core с точки зрения архитектуры приложения?
(1) C точки зрения архитектуры приложения, нужно обеспечить чтобы код доступа к вашей базе данных был изолирован / отделен в четко определенном месте (в изоляции). Это позволяет найти код базы данных, который влияет на производительность.
(2) Не смешивать код доступа к вашей базе данных с другими частями приложения, такими как пользовательский интерфейс или API. Таким образом, код доступа к базе данных можно изменить, не беспокоясь о других проблемах, не связанных с базой данных.
Как правильно и быстро сохранять данные с помощью SaveChanges?
Если вставляемые записи одинаковые имеет смысл использовать одну операцию сохранения на все записи.
Неправильно
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();
}
}
Правильно
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();
}
}
Всегда есть исключения из правила. Если контекст транзакции сложный, то есть состоит из нескольких независимых операций, то можно выполнять сохранение после выполнения каждой операции. А еще правильней использовать асинхронное сохранение в транзакции.
// Увеличение депозита его владельца
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();
}
}
}
Триггеры, вычисляемые поля, пользовательские функции и EF Core
Для снижения нагрузки на приложения содержащим EF Core имеет смысл применять простые вычисляемые поля и триггеры баз данных, но лучше этим не увлекаться, так как приложение может оказаться очень запутанным. А вот пользовательские функции могут быть очень полезны особенно при операциях выборки!
Параллелизм в EF Core
Если ты хочешь все запараллелить чтобы ускориться, то обломись: EF Core не поддерживает выполнение нескольких параллельных операций в одном экземпляре контекста. Следует подождать завершения одной операции, прежде чем запускать следующую. Для этого обычно нужно указать ключевое слово await в каждой асинхронной операции.
EF Core использует асинхронные запросы, которые позволяют избежать блокирования потока при выполнении запроса в базе данных. Асинхронные запросы важны для обеспечения быстрого отклика пользовательского интерфейса в толстых клиентах. Они могут также увеличить пропускную способность в веб-приложении, где можно высвободить поток для обработки других запросов. Вот пример:
public async Task<List<Blog>> GetBlogsAsync()
{
using (var context = new BloggingContext())
{
return await context.Blogs.ToListAsync();
}
}
А что вы знаете про компилированные запросы LINQ?
Если у вас есть приложение, которое многократно выполняет структурно похожие запросы в Entity Framework, вы часто можете повысить производительность, компилируя запрос один раз и выполняя его несколько раз с различными параметрами. Например, приложению может потребоваться получить всех клиентов в определенном городе; город указывается во время выполнения пользователем в форме. LINQ to Entities поддерживает использование для этой цели скомпилированных запросов.
Начиная с .NET Framework 4.5, запросы LINQ кэшируются автоматически. Тем не менее, вы все равно можете использовать скомпилированные запросы LINQ, чтобы снизить эту стоимость в последующих выполнениях, и скомпилированные запросы могут быть более эффективными, чем запросы LINQ, которые автоматически кэшируются. Обратите внимание, что запросы LINQ to Entities, которые применяют оператор Enumerable.Contains к коллекциям в памяти, не кэшируются автоматически. Также не допускается параметризация коллекций в памяти в скомпилированных запросах LINQ.
Много примеров можно посмотреть тут.
Не делайте больших контекстов DbContext!
В общем так, я знаю многие из вас, если не почти все — lazy f_u__c_k__e_r__s и всю базу данных вы размещаете в один контекст, особенно это свойственно для подхода Database-First. И зря вы это делаете! Ниже приведен пример как можно разделить контекст. Конечно, таблицы соединения между контекстами придется дублировать, это минус. Так или иначе если у вас в контексте более 50 таблиц лучше подумать о его разделении.
Использование группировки контекста (pooling DdContext)
Смысл пула DbContext состоит в том, чтобы разрешить повторное использование экземпляров DbContext из пула, что в некоторых случаях может привести к повышению производительности по сравнению с созданием нового экземпляра каждый раз. Это также является основной причиной создания пула соединений в ADO.NET, хотя прирост производительности для соединений будет более значительным, поскольку соединения, как правило, являются более тяжелым ресурсом.
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();
}
Как избежать лишних ошибок при CRUD в EF Core?
Никогда не делайте вычисления в вставку в одном коде. Всегда разделяйте формирование/подготовку объекта и его вставку/обновление. Просто разнесите по функциям: проверку введенных данным пользователем, вычисления необходимые предварительных данных, картирование или создание объекта, и собственно CRUD операцию.
Что делать, когда совсем дела плохо с производительностью приложения?
Пиво тут точно не поможет. А вот что поможет, так это разделение чтение и записи в архитектуре приложения с последующего разнесением по сокетам этих операций. Задумайтесь об использовании Command and Query Responsibility Segregation (CQRS) pattern, а также попробуйте, разделить таблицы на вставку и чтение между двумя базами данных.
Скоростных приложений вам, друзья и коллеги!