Как улучшить производительность доступа к данным в EF Core

автор vadim


Entity Framework Core (EF Core) — это платформа ORM (объектно-реляционное сопоставление) с открытым исходным кодом, которая устраняет разрыв между объектной моделью вашего приложения и моделью данных вашей базы данных. EF Core упрощает жизнь, позволяя работать с базой данных с использованием объектов .NET вместо написания кода доступа к данным.

Другими словами, EF Core позволяет вам писать код для выполнения действий CRUD (создание, чтение, обновление и удаление), не понимая, как данные сохраняются в базовой базе данных. Вы можете легче извлекать сущности из хранилища данных, добавлять, изменять и удалять сущности, а также перемещаться по графам сущностей, работая непосредственно в C#.

Вы можете повысить производительность доступа к данным в EF Core разными способами: от использования быстрой загрузки до сокращения циклических обращений к базе данных, необходимых для ваших запросов. В этой статье мы рассмотрим 10 советов, приемов и стратегий, которые можно использовать в EF Core для повышения производительности доступа к данным в наших приложениях .NET Core.

Для работы с приведенными ниже примерами кода в вашей системе должна быть установлена ​​Visual Studio 2022. Если у вас еще нет копии, вы можете скачать Visual Studio 2022 здесь.

Создайте проект консольного приложения в Visual Studio.

Прежде всего давайте создадим проект консольного приложения .NET Core в Visual Studio. Предполагая, что в вашей системе установлена ​​Visual Studio 2022, выполните действия, описанные ниже, чтобы создать новый проект консольного приложения .NET Core.

  1. Запустите интегрированную среду разработки Visual Studio.
  2. Нажмите «Создать новый проект».
  3. В окне «Создать новый проект» выберите «Консольное приложение (.NET Core)» из списка отображаемых шаблонов.
  4. Нажмите “Далее.
  5. В окне «Настроить новый проект» укажите имя и местоположение нового проекта.
  6. Нажмите “Далее.
  7. В показанном ниже окне «Дополнительная информация» выберите «.NET 7.0 (стандартный срок поддержки)» в качестве версии Framework, которую вы хотите использовать.
  8. Нажмите Создать.

В этой статье мы будем использовать этот проект для работы с EF Core 7. В следующих разделах мы обсудим 10 способов повышения скорости доступа к данным в EF Core, проиллюстрированных примерами кода, где это необходимо. Давайте начнем!

Получайте только те данные, которые вам нужны

Имея дело с огромными объемами данных, вам следует стремиться получить только те записи, которые необходимы для конкретного запроса. При получении данных следует использовать проекции, чтобы выбирать только необходимые поля и избегать получения ненужных полей.

В следующем фрагменте кода показано, как получить данные в страничном виде. Обратите внимание, как индекс начальной страницы и размер страницы используются для выбора только необходимых данных.

int pageSize = 50, startingPageIndex = 1;
var dataContext = new OrderProcessingDbContext();
var data = dataContext.Orders.Take(pageSize)
.Skip(startingPageIndex * pageSize)
.ToList();

Разделите большой контекст данных на множество более мелких контекстов данных.

Контекст данных в вашем приложении представляет вашу базу данных. Следовательно, вы можете задаться вопросом, должно ли приложение иметь только один или несколько контекстов данных. В Entity Framework Core время запуска контекста больших данных представляет собой значительное ограничение производительности. В результате вместо использования одного обширного контекста данных вам следует разбить контекст данных на множество более мелких контекстов данных.

В идеале у вас должен быть только один контекст данных для каждого модуля или единицы работы. Чтобы использовать несколько контекстов данных, просто создайте новый класс для каждого контекста данных и расширите его из класса DbContext.

Используйте пакетные обновления для большого количества объектов.

Поведение EF Core по умолчанию — отправлять отдельные инструкции обновления в базу данных, когда необходимо выполнить пакет инструкций обновления. Естественно, множественные обращения к базе данных влекут за собой значительные потери производительности. Чтобы изменить это поведение и оптимизировать пакетные обновления, вы можете воспользоваться методом UpdateRange(), как показано в приведенном ниже фрагменте кода.

public class DataContext : DbContext
  {
      public void BatchUpdateAuthors(List<Author> authors)
      {
          var students = this.Authors.Where(a => a.Id >10).ToList();
          this.UpdateRange(authors);
          SaveChanges();
      }
      protected override void OnConfiguring
      (DbContextOptionsBuilder options)
      {
          options.UseInMemoryDatabase("AuthorDb");
      }
      public DbSet<Author> Authors { get; set; }
      public DbSet<Book> Books { get; set; }
  }

Если вы используете EF Core 7 или более позднюю версию, вы можете использовать методы ExecuteUpdate и ExecuteDelete для выполнения пакетных обновлений и устранения множественных обращений к базе данных. Например:

_context.Authors.Where(a => a.Id > 10).ExecuteUpdate();

Отключить отслеживание изменений для запросов только для чтения

Поведение EF Core по умолчанию — отслеживание объектов, полученных из базы данных. Отслеживание необходимо, когда вы хотите обновить объект новыми данными, но это дорогостоящая операция, когда вы имеете дело с большими наборами данных. Следовательно, вы можете повысить производительность, отключив отслеживание, когда вы не собираетесь изменять объекты.

Для запросов только для чтения, т. е. когда вы хотите получить объекты без их изменения, вам следует использовать AsNoTracking для повышения производительности. В следующем фрагменте кода показано, как можно использовать AsNoTracking для отключения отслеживания отдельного запроса в EF Core.

var dbModel = await this._context.Authors.AsNoTracking()
    .FirstOrDefaultAsync(e => e.Id == author.Id);

Приведенный ниже фрагмент кода показывает, как можно извлекать сущности непосредственно из базы данных только для чтения, без отслеживания и загрузки их в память.

public class DataContext : DbContext
{
    public IQueryable<Author> GetAuthors()
    {
        return Set<Author>().AsNoTracking();
    }
}

Использовать пул DbContext

Приложение обычно имеет несколько контекстов данных. Поскольку создание и удаление объектов DbContext может оказаться дорогостоящим, EF Core предлагает механизм их объединения в пул. При объединении в пул объекты DbContext создаются один раз, а затем повторно используются при необходимости.

Использование пула DbContext в EF Core может повысить производительность за счет снижения накладных расходов, связанных с созданием и удалением объектов DbContext. В результате ваше приложение также может использовать меньше памяти.

В следующем фрагменте кода показано, как можно настроить пул DbContext в файле Program.cs.

builder.Services.AddDbContextPool<MyDbContext>(options => options.UseSqlServer(connection));

Используйте IQueryable вместо IEnumerable

При запросе данных в EF Core используйте IQueryable вместо IEnumerable. Когда вы используете IQueryable, операторы SQL будут выполняться на стороне сервера, где хранятся данные, тогда как IEnumerable требует, чтобы запрос выполнялся на стороне клиента. Более того, хотя IQueryable поддерживает оптимизацию запросов и отложенную загрузку, IEnumerable этого не делает. Это объясняет, почему IQueryable выполняет запросы быстрее, чем IEnumerable.

В следующем фрагменте кода показано, как можно использовать IQueryable для запроса данных.

IQueryable<Author> query = _context.Authors;
query = query.Where(e => e.Id == 5);
query = query.OrderBy(e => e.Id);
List<Author> entities = query.ToList();

Используйте нетерпеливую загрузку вместо отложенной загрузки

EF Core по умолчанию использует отложенную загрузку. При отложенной загрузке связанные объекты загружаются в память только при доступе к ним. Преимущество заключается в том, что данные не загружаются, если они не нужны. Однако отложенная загрузка может оказаться дорогостоящей с точки зрения производительности, поскольку для загрузки данных может потребоваться несколько запросов к базе данных.

Чтобы решить эту проблему для конкретных сценариев, вы можете использовать быструю загрузку в EF Core. При быстрой загрузке ваши сущности и связанные сущности извлекаются в одном запросе, что сокращает количество обращений к базе данных. Следующий фрагмент кода показывает, как можно использовать быструю загрузку.

public class DataContext : DbContext
{
    public List<Author> GetEntitiesWithEagerLoading()
    {
        List<Author> entities = this.Set<Author>()
            .Include(e => e.Books)
            .ToList();
        return entities;
    }
}

Отключить отложенную загрузку

Устраняя необходимость загрузки ненужных связанных объектов (как при явной загрузке), ленивая загрузка, похоже, полностью освобождает разработчика от работы со связанными объектами. Поскольку EF Core умеет автоматически загружать связанные объекты из базы данных при доступе к ним вашего кода, отложенная загрузка кажется хорошей функцией.

Однако отложенная загрузка особенно склонна к созданию ненужных дополнительных циклов обработки, что может замедлить работу приложения. Вы можете отключить отложенную загрузку, указав следующее в контексте данных:

ChangeTracker.LazyLoadingEnabled = false;

Используйте асинхронный вместо синхронного кода

Вам следует использовать асинхронный код, чтобы улучшить производительность и скорость реагирования вашего приложения. Ниже я поделюсь примером кода, который показывает, как можно асинхронно выполнять запросы в EF Core. Сначала рассмотрим следующие два класса моделей.

public class Author
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Book> Books { get; set; }
}
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public Author Author { get; set; }
}

В следующем фрагменте кода мы создадим пользовательский класс контекста данных, расширив класс DbContext библиотеки EF Core.

public class DataContext : DbContext
{
    protected readonly IConfiguration Configuration;
    public DataContext(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    protected override void OnConfiguring
    (DbContextOptionsBuilder options)
    {
        options.UseInMemoryDatabase("AuthorDb");
    }
    public DbSet<Author> Authors { get; set; }
    public DbSet<Book> Books { get; set; }
}

Обратите внимание, что для простоты мы используем базу данных в памяти. В следующем фрагменте кода показано, как можно использовать асинхронный код для обновления объекта в базе данных с помощью EF Core.

public async Task<int> Update(Author author)
{
    var dbModel = await this._context.Authors
       .FirstOrDefaultAsync(e => e.Id == author.Id);
       dbModel.Id = author.Id;
       dbModel.FirstName = author.FirstName;
       dbModel.LastName = author.LastName;
       dbModel.Books = author.Books;
       return await this._context.SaveChangesAsync();
}

Уменьшите количество обращений к базе данных

Вы можете значительно сократить количество обращений к базе данных, избежав проблемы выбора N+1. Проблема выбора N+1 ухудшала производительность базы данных с первых дней существования ORM. Название относится к проблеме отправки N+1 небольших запросов к базе данных для получения данных, которые можно получить с помощью одного большого запроса.

В EF Core проблема N+1 может возникнуть, когда вы пытаетесь загрузить данные из двух таблиц, имеющих отношение «один ко многим» или «многие ко многим». Например, предположим, что вы загружаете данные об авторах из таблицы «Авторы», а также данные о книгах из таблицы «Книги». Рассмотрим следующий фрагмент кода.

foreach (var author in this._context.Authors)
{
    author.Books.ForEach(b => b.Title.ToUpper());
}

Обратите внимание, что внешний цикл foreach выберет всех авторов с помощью одного запроса. Это цифра «1» в ваших запросах N+1. Внутренний foreach, извлекающий книги, представляет собой букву «N» в вашей задаче N+1, поскольку внутренний foreach будет выполнен N раз.

Чтобы решить эту проблему, вам следует заранее получить связанные данные (с использованием быстрой загрузки) как часть запроса «1». Другими словами, вам следует включить данные книги в первоначальный запрос данных об авторе, как показано в приведенном ниже фрагменте кода.

var entitiesQuery = this._context.Authors
    .Include(b => b.Books);
foreach (var entity in entitiesQuery)
{
   entity.Books.ForEach(b => b.Title.ToUpper());
}

Поступая таким образом, вы сокращаете количество обращений к базе данных с N+1 до одного. Это связано с тем, что, используя Include, мы активируем быструю загрузку. Внешний запрос, т. е.entitiesQuery, выполняется только один раз, чтобы загрузить все записи об авторе вместе с соответствующими данными книги. Вместо обращения к базе данных два цикла foreach работают с доступными данными в памяти.

Кстати, EF Core 7 бесплатно сокращает количество обращений к базе данных. Управление транзакциями для одиночных операторов вставки было удалено из EF Core 7, поскольку в нем больше нет необходимости. В результате в EF Core 7 отсутствуют два цикла обработки данных, которые использовались в предыдущих версиях EF Core для начала и фиксации транзакции. В результате EF Core 7 обеспечивает значительный прирост производительности при вставке данных в базу данных с помощью одного оператора вставки по сравнению с предшественниками.

Производительность должна быть особенностью

В этой статье мы рассмотрели 10 ключевых стратегий, которые можно использовать для повышения производительности доступа к данным в EF Core. Кроме того, вам следует точно настроить структуру базы данных, индексы, запросы и хранимые процедуры, чтобы получить максимальную выгоду. Производительность должна быть особенностью вашего приложения. Крайне важно с самого начала учитывать производительность при создании приложений, использующих большой объем данных.

Наконец, каждое приложение имеет разные требования и характеристики доступа к данным. Вам следует оценить производительность EF Core до и после применения любых изменений, которые мы обсуждали здесь, чтобы оценить результаты для вашего конкретного приложения. Отличным инструментом для этой задачи является BenchmarkDotNet, о котором вы можете прочитать в моем предыдущем посте здесь.

Дальше читайте это:

  • Облачные вычисления больше не являются пустяком
  • Что такое генеративный ИИ? Искусственный интеллект, который создает
  • Программирование с помощью ИИ: советы и лучшие практики от разработчиков
  • Python пытается удалить GIL и повысить параллелизм
  • 7 причин, по которым Java по-прежнему хороша
  • Война за лицензирование открытого исходного кода окончена

Related Posts

Оставить комментарий