- Добавление настроек базы данных при ее инициализации
- Создание базы данных в зависимости от условия
- Класс контекста данных
- Пре-компиляция LINQ выражений в SQL
- Подключение и создание базы данных в Entity Framework Core
- Использование DbContext. Entry для редактирования
- Запуск инициализации базы данных вручную
- Установка Entity Framework 6 в проект
- Создание проекта
- System Under Test (SUT)
- Определение классов модели
- Работа с данными
- DbContext pooling
- Создание базы данных
- Влияние комбинирования улучшений на производительность
- Отключение внутренних проверок потокобезопасности
- Создание базы данных с некоторыми данными по умолчанию
- Отключение отслеживания изменений в объектах для read-only запросов
Добавление настроек базы данных при ее инициализации
Помимо добавления данных по умолчанию, в базе данных можно определить и другие настройки, которые будут применены при инициализации. Например, вы можете создать индекс для столбца FirstName таблицы Customers, чтобы ускорить поиск по имени покупателя в этой таблице. Чтобы определить различные настройки, вы можете использовать метод DbContext. Database. ExecuteSqlCommand()
, в который передается произвольная SQL-команда для настройки различных аспектов базы данных. Этот метод вызывается также, в переопределенном методе Seed() объекта инициализации. В следующем примере показано, как задать индекс для столбца CustomerId:
protected override void Seed(SampleContext context)
{
context.Database.ExecuteSqlCommand
("CREATE INDEX Index_Customer_Name ON Customers (CustomerId) INCLUDE (FirstName)");
base.Seed(context);
}
Очевидно, что с помощью этого метода вы можете выполнить любую SQL-команду для настройки базы данных. На рисунке ниже показан добавленный в таблицу индекс:
Создание базы данных в зависимости от условия
С помощью метода Database. SetInitializer()
можно управлять поведением Code-First для создания базы данных при изменении модели. Этот метод принимает экземпляр интерфейса IDatabaseInitializer<TContext>
. В Entity Framework есть три класса, реализующих этот интерфейс и обеспечивающих возможность выбора поведения Code-First при инициализации базы данных:
- CreateDatabaseIfNotExists
Экземпляр этого класса используется в Code-First по умолчанию для всех классов контекста. Это безопасный способ инициализации, при котором база данных никогда не будет удалена и данные не будут потеряны. При этом способе инициализации, база данных создается только один раз, когда ее еще не существует. Если модель данных была изменена, например мы добавили новый класс, то Entity Framework обнаружит эти изменения (с помощью таблицы __MigrationHistory) и возбудит исключение, т.к. при таком типе инициализации нельзя удалить базу данных, а соответственно нельзя отразить на нее новую модель.
- DropCreateDatabaseIfModelChanges
Пример использования этого инициализатора вы видели в начале статьи. Он указывает на то, что если модель данных изменилась и она не соответствует текущей структуре базы данных, то эту базу данных нужно удалить, а затем воссоздать с использованием новой модели. Очевидно, что такая инициализация удобна на этапе разработке приложения, но не подходит при развертывании приложения, когда простое изменение модели удалит все данные из базы.
- DropCreateDatabaseAlways
При этом типе инициализации база данных будет удаляться и создаваться заново при каждом запуске приложения, независимо от того, изменилась ли модель данных. Данный тип инициализации используется крайне редко.
Помимо этих стандартных типов инициализации вы можете создать произвольный тип инициализации, реализовав интерфейс IDatabaseInitializer. В этом интерфейсе определен один обобщенный метод InitializeDatabase()
принимающий объект контекста. Примером пользовательского механизма инициализации может послужить класс DontDropDbJustCreateTablesIfModelChanged, который находится в расширении EF CodeFirst пакета NuGet. С помощью этого инициализатора вы можете не беспокоится об удалении базы данных, т.к. он затрагивает только таблицы, которые изменились в модели.
Установить инициализатор, используемый по умолчанию, можно также в конфигурационном файле приложения, как показано в примере ниже:
<entityFramework>
<contexts>
<context type="CodeFirst.SampleContext, CodeFirst" disableDatabaseInitialization="false">
<databaseInitializer
type="System.Data.Entity.DropCreateDatabaseIfModelChanges`1[[CodeFirst.SampleContext, CodeFirst]], EntityFramework" />
</context>
</contexts>
...
</entityFramework>
Для этого используется раздел contexts
настроек Entity Framework. При структуре этого приложения эти настройки нужно указывать в файле Web.config веб-приложения, а не в проекте, где мы создавали модель. Обратите внимание, что в этом примере показано использование атрибута disableDatabaseInitialization. Если вы установите его в true, то сможете отключить автоматическую инициализацию базы данных в приложении, как мы делали это с использованием метода Initialize(null). В атрибуте type узла context указывается полное имя класса контекста, а также имя сборки, где содержится этот класс. В атрибуте type узла databaseInitializer указывается полный тип инициализатора, обратите внимание на синтаксис этой инструкции.
Класс контекста данных
Сами по себе классы модели, созданные ранее, не имеют ничего общего с Entity Framework. На данном этапе они просто описывают структуру бизнес-модели, которая используется в приложении. Чтобы Entity Framework был в курсе, что эти классы служат также для управления данными в базе данных, нужно использовать класс контекста. E F имеет два базовых класса контекста:
- ObjectContext
Этот класс является более общим классом контекста данных, и используется начиная с самых ранних версий Entity Framework.
- DbContext
Этот класс контекста данных появился в Entity Framework 4.1 и он обеспечивает поддержку подхода Code-First (ObjectContext также обеспечивает работу подхода Code-First, но он труднее в использовании). Далее мы будем использовать DbContext.
Для создания класса контекста добавьте следующий новый класс SampleContext в проект CodeFirst:
using System.Data.Entity;
namespace CodeFirst
{
public class SampleContext : DbContext
{
// Имя будущей базы данных можно указать через
// вызов конструктора базового класса
public SampleContext() : base("MyShop")
{ }
// Отражение таблиц базы данных на свойства с типом DbSet
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
}
}
Этот небольшой класс контекста представляет полный слой данных, который можно использовать в приложениях. Благодаря DbContext, вы сможете запросить, изменить, удалить или вставить значения в базу данных. Обратите внимание на использование конструктора в этом классе с вызовом конструктора базового класса DbContext и передачей ему строкового параметра. В этом параметре указывается либо имя базы данных либо строка подключения к базе данных (Entity Framework достаточно интеллектуален чтобы отличить тип параметра). В данном случае мы указываем явно имя базы данных, т.к. по умолчанию, при генерации базы данных Entity Framework использует имя приложения и контекста данных (например CodeFirst. SampleContext), которое нам не подходит.
После этого скомпилируйте приложение (горячая клавиша F6
), чтобы исключить наличие ошибок на данном этапе.
Пре-компиляция LINQ выражений в SQL
private readonly AdventureWorksContext _context;
...
private static Func<AdventureWorksContext, int, CancellationToken, Task<Product>> _getProductByIdQuery =
EF.CompileAsyncQuery<AdventureWorksContext, int, Product>((ctx, productId, ct) =>
ctx.Products.AsQueryable().FirstOrDefault(x => x.ProductId == productId));
...
public async Task<Product> GetProduct(int productId, CancellationToken cancellationToken = default)
{
return await _getProductByIdQuery.Invoke(_context, productId, cancellationToken);
}
Несмотря на ожидаемые преимущества от применения такого подхода, а именно уменьшение аллокаций и уменьшение использования CPU, стоит отметить и недостатки. Во-первых, как можно заметить из примера, код стал значительно менее удобен для чтения. Во-вторых, для использования этого подхода вам необходимо затратить значительно больше времени чем на добавление AsNoTracking
, особенно для переписывания и тестирования уже существующего кода. Отдельно хотелось бы отметить, на мой взгляд, не очень подробную документацию данной возможности и немного запутанный интерфейс метода EF.CompileAsyncQuery
. С имеющейся документацией можно ознакомиться по этой ссылке
.
Подключение и создание базы данных в Entity Framework Core
Данное руководство устарело. Актуальное руководство: Руководство по ASP. NET Core 7
Последнее обновление: 09.12.2019
Entity Framework представляет прекрасное ORM-решение, которое позволяет автоматически связать обычные классы языка C# с таблицами в базе данных. Entity Framework Core
нацелен в первую очередь на работу с СУБД MS SQL Server, однако поддерживает также и ряд других СУБД. В данном случае мы будем работать с базами данных в MS SQL Server.
Также стоит отметить, что здесь мы будем использовать Entity Framework Core
— кроссплатформенное решение на базе . NET Core,
которое отличается от предыдущих версий, например, от Entity Framework 6. Более подробно с работой с Entity Framework Core можно ознакомиться в соответствуюшем
руководстве
.
Для работы с Entity Framework вначале создадим новый проект ASP. NET Core по шаблону ASP. NET Core Web App (Model-View-Controller). Пусть он будет называться .
Для взаимодействия с MS SQL Server через Entity Framework необходим пакет .
По умолчанию он отсутствует в проекте, поэтому его надо добавить, например, через пакетный менеджер Nuget:
public class User { public int Id { get; set; } public string Name { get; set; } // имя пользователя public int Age { get; set; } // возраст пользователя }
Эта модель представляет те объекты, которые будут храниться в базе данных.
Чтобы взаимодействовать с базой данных через Entity Framework нам нужен контекст данных — класс, унаследованный от класса
. Поэтому добавим в папку Models новый класс, который назовем (название класса контекста произвольное):
using Microsoft. EntityFrameworkCore; namespace EFDataApp. Models { public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { Database. EnsureCreated(); // создаем базу данных при первом обращении } } }
Через параметр options
в конструктор контекста данных будут передаваться настройки контекста.
В конструкторе с помощью вызова Database.EnsureCreated()
по определению моделей будет создаваться база данных (если она отсутствует).
Чтобы подключаться к базе данных, нам надо задать параметры подключения. Для этого изменим файл , добавив в него
определение строки подключения:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=mobilesdb;Trusted_Connection=True;" }, // остальное содержимое файла }
В данном случае мы будем использовать упрощенный движок базы данных LocalDB, который представляет легковесную версию
SQL Server Express, предназначенную специально для разработки приложений.
И последним шагом в настройке проекта является изменение файла . В нем нам надо изменить метод ConfigureServices()
:
using EFDataApp. Models; using Microsoft. AspNetCore. Builder; using Microsoft. AspNetCore. Hosting; using Microsoft. Extensions. Configuration; using Microsoft. Extensions. DependencyInjection; using Microsoft. Extensions. Hosting; using Microsoft. EntityFrameworkCore; namespace EFDataApp { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // получаем строку подключения из файла конфигурации string connection = Configuration. GetConnectionString("DefaultConnection"); // добавляем контекст ApplicationContext в качестве сервиса в приложение services. AddDbContext(options => options. UseSqlServer(connection)); services. AddControllersWithViews(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env. IsDevelopment()) { app. UseDeveloperExceptionPage(); } else { app. UseExceptionHandler("/Home/Error"); app. UseHsts(); } app. UseHttpsRedirection(); app. UseStaticFiles(); app. UseRouting(); app. UseAuthorization(); app. UseEndpoints(endpoints => { endpoints. MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } } }
Добавление контекста данных в виде сервиса позволит затем получать его в конструкторе контроллера через механизм внедрения зависимостей.
Использование DbContext. Entry для редактирования
Отдельно стоит выделить и разобрать результат тестирования сценария редактирования (Edit product), в котором Dapper превосходит EF более чем в 2 раза. Такой результат очень просто объяснить, взглянув на код редактирования в версии IProductsRepository
для EF:
...
var bookProduct = dbContext.Products.Where(p => p.Name == "Harry Potter").Single(); // < -- 1-st query to db
bookProduct.Name = "Harry Potter and the Sorcerer's Stone"
context.SaveChanges(); // <-- 2-nd query to DB
...
Для выполнения редактирования с помощью C# нам необходимо сначала получить объект, выполнив запрос в базу данных, модифицировать его и вызвать SaveChanges
, что отправит еще один запрос в базу данных. Двукратное превосходство Dapper объясняется тем, что EF для редактирования с использованием C# необходимо отправлять в 2 раза больше запросов. Однако в EF есть еще один способ редактирования с использованием C#, который позволяет выполнить всего один запрос. Для этого нам необходимо вручную создать экземпляр Product
, присвоить ему нужные свойства и вручную отредактировать состояние объекта в системе отслеживания изменений, при необходимости выбирая только те свойства, которые мы хотим поменять. В нашем случае мы собираемся менять только название:
public async Task EditProductName(int productId, string productName)
{
var product = new Product { ProductId = productId, Name = productName };
_context.Products.Attach(product);
_context.Entry(product).Property(x => x.Name).IsModified = true;
try
{
_ = await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// exception is throws when @@ROWCOUNT is equal to 0
// which means no entity with such Id was updated
throw new ProductNotFoundException();
}
}
Запуск инициализации базы данных вручную
Есть ситуации, когда вы, возможно, захотите контролировать момент создания базы данных, а не использовать автоматическую инициализацию Code-First. Для явного вызова процесса создания базы данных используется метод DbContext. Database. Initialize()
, которому передается логический параметр. Если этот логический параметр равен false, то инициализация запустится только в том случае, если она уже не была вызвана ранее. Если этот логический параметр равен true, инициализация запустится в любом случае. Стоит запомнить, что этот метод следует вызвать до создания объекта контекста, его, например, можно указать в конструкторе класса контекста.
Возникает вопрос, когда может пригодиться ручная инициализация? С помощью ручной инициализации вы можете обработать любые ошибки, которые возникают во время создания модели и базы данных, в одном месте. Еще одной причиной является увеличение производительности, при отражении большой и сложной модели данных.
Давайте рассмотрим пример использования ручной инициализации. Мы будем использовать старую модель, с двумя классами Customer и Order, которую использовали ранее. Следующий код можно добавить в обработчик загрузки веб-формы нашего приложения ASP. NET:
protected void Page_Load(object sender, EventArgs e)
{
try
{
SampleContext context = new SampleContext();
// Запустить инициализацию базы данных в этой точке
context.Database.Initialize(false);
}
catch (Exception ex)
{
// Если при создании БД возникла ошибка,
// отобразим ее в окне отладчика
Debug.WriteLine("Инициализация не выполнена. Ошибка: ");
Debug.WriteLine(ex.Message);
}
}
Давайте теперь создадим ошибку в нашей модели. Для этого первичному ключу CustomerId класса Customer явно зададим тип NVARCHAR в базе данных с помощью атрибута Column. Это вызовет ошибку инициализации модели, т.к. поле имеет тип int:
public class Customer
{
[Column(TypeName="nvarchar")]
public int CustomerId { get; set; }
// ...
}
В результате запуска приложения с такой моделью, в окне отладки Output вы увидите следующее сообщение с ошибкой:
Как вы видите, с помощью ручной инициализации мы можем обработать ошибки, возникающие при отображении модели на базу данных. В данном случае мы просто вывели сообщение в консоль, но в реальном приложении вы можете предпринять какие-нибудь действия и попробовать повторно инициализировать базу данных. Удалите атрибут Column у свойства CustomerId в классе модели, чтобы тестировать последующие примеры.
С помощью метода Initialize() можно также отключить автоматическую инициализацию базы данных. Для этого вы можете передать ему значение null, при этом все еще можно будет запустить инициализацию вручную.
Установка Entity Framework 6 в проект
Всякий раз, когда вам впервые понадобится использовать Entity Framework в проекте при подходе Code-First, вы должны будете добавить ссылки на библиотеки EF, после чего можно будет работать с Entity Framework в коде. Используйте для этого следующие шаги:
Начиная с версии 4, библиотека Entity Framework входит в удобный менеджер пакетов NuGet
. Чтобы добавить поддержку в Entity Framework с помощью NuGet, выберите в окне Solution Explorer проект CodeFirst, щелкните по нему правой кнопкой мыши и выполните команду из контекстного меню Manage Nuget Packages.В появившемся диалоговом окне выберите последнюю версию Entity Framework на вкладке Online -> nuget.org и нажмите кнопку Install:
После этого появится окно с описанием лицензии на использование Entity Framework. Согласитесь с условиями и на жмите кнопку “I accept”, после чего NuGet установит в ваш проект Entity Framewrok.
После этого добавьте ссылку на ключевое пространство имен в Entity Framework — System. Data. Entity в файле Model.cs:
using System.Data.Entity;
Повторите эти действия без 4 пункта для установки Entity Framework в проект ASP. NET.
Создание проекта
Во всех статьях на нашем сайте, которые посвящены описанию Entity Framework, мы используем проект простого веб-приложения ASP. NET. В этом разделе мы покажем как создать этот проект, а позже будем ссылаться на эту статью, чтобы каждый раз не описывать одни и те же шаги:
Запустите Visual Studio 2012 (в примерах всех статей мы будем использовать версию Visual Studio 2012 в сочетании с Entity Framework 6).
Создайте новый проект ASP. NET, выбрав в меню File —> New Project. После этого откроется диалоговое окно, в котором укажите шаблон приложения ASP. NET Empty Web Application. Назовите произвольно проект, выберите папку сохранения и нажмите кнопку OK.
В созданном проекте щелкните правой кнопкой мыши по имени решения в окне Solution Explorer и выберите в контекстном меню команду Add —> New Project.
Добавьте в решение проект, имеющий шаблон библиотеки классов (Class Library) и назовите его CodeFirst:
Добавьте в новый проект файл класса Model.cs, в котором мы будем описывать модель данных.
Добавьте ссылку на проект CodeFirst в базовом проекте веб-приложения. Для этого щелкните правой кнопкой мыши по вкладке References в окне Solution Explorer базового проекта и выберите пункт Add Reference. В открывшемся диалоговом окне перейдите на вкладку Solution и выберите проект CodeFirst.
System Under Test (SUT)
Для демонстрации и сравнения нам понадобится веб API, которое будет взаимодействовать с тестовой SQL базой AdventureWorks, реализуя несколько часто встречающихся сценариев:
GET запрос по Id с данными из одной таблицы. Get product by Id
;GET запрос по Id с данными из нескольких связанных таблиц (JOIN-s). Get product with model and product category by id
;GET запрос страницы с данными из одной таблицы. Get products page
;GET запрос страницы с данными из нескольких связанных таблиц (JOIN-s). Get products page with model and product category datas
;POST запрос на создание. Create product
PUT запрос на редактирование. Edit product name
.
Нам понадобится реализовать API несколько раз, используя разные имплементации IProductsRepository
на базе EF или Dapper для доступа к данным. Для полноценного нагрузочного тестирования мы будем использовать NBomber поочередно для всех перечисленных сценариев. Подробнее о NBomber и работе с ним можно ознакомится в этой статье
. Для более быстрых локальных тестов в некоторых случаях мы будем использовать BenchmarkDotNet сценарии
, которые будут повторять наше API в миниатюре, вызывая разные реализации интерфейса IProductsRepository
для EF, Dapper и вариаций EF с различными улучшениями:
private ServiceProvider EFCoreDefaultImplementationServiceProvider;
...
[GlobalSetup]
public void GlobalSetup()
{
BuildDefaultImplementationServiceProvider();
}
...
[Benchmark]
public async Task GetProduct_Benchmark()
{
// we will do several iterations, emulating several requests, to see difference in time and memory better
for (int i = 0; i < IterationsCount; i++)
{
// as for each HTTP request in web api, we will create DI scope
using var scope = EFCoreDefaultImplementationServiceProvider.CreateScope();
// ... from which we will resolve implementation under test
var repository = scope.ServiceProvider.GetRequiredService<IProductsRepository>();
// get product by id scenario as example
var product = await repository.GetProduct(ProductIds[i % (ProductIds.Length - 1)]);
}
}
После того как мы рассмотрим все рекомендации
по улучшению производительности работы EF, мы проведем еще один NBomber тест с примененными улучшениями и после сможем сделать выводы. Весь код использованный в данной статье доступен в репозитории на Github
.
Перед началом улучшений проведем замер для Dapper и версии EF «из коробки». Для теста запустим поочередно обе версии приложения и проведем последовательное нагрузочное тестирование для каждого из сценариев, используя 30 тестовых клиентов, безостановочно шлющих запросы.
Как видим в данной конфигурации EF на 19-30
процентов уступает Dapper в большинстве сценариев для чтения, и значительно уступает в сценариях создания и редактирования. Теперь мы имеем точку отсчета и можем приступить к работе над улучшениями.
Определение классов модели
Как вы уже знаете, при использовании подхода Code-First сначала определяется модель в коде, а затем, на ее основе создается (или модифицируется) база данных. Для нашего примера потребуется создать два класса, описывающих заказчика и его товары. Эти классы добавляются в файл Model.cs, созданный в предыдущем разделе:
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public byte[] Photo { get; set; }
// Ссылка на заказы
public virtual List<Order> Orders { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public DateTime PurchaseDate { get; set; }
// Ссылка на покупателя
public Customer Customer { get; set; }
}
Этот кусок кода определяет данные заказчика и его покупки. Для каждого заказчика указывается имя, email, возраст и фотография профиля. Идентификатор используется в качестве первичного ключа таблицы Customer. Кроме того, в этом классе есть ссылка на коллекцию покупок. Эта ссылка выражена в виде виртуального свойства и имеет тип обобщенной коллекции List<T>.
Класс Order содержит идентификатор заказа, который позволяет уникальным образом распознать каждый заказ в таблице. Кроме того этот класс содержит свойства, описывающие название товара, его количество, описание и дату заказа. Также здесь указана ссылка на покупателя в виде свойства Customer.
Работа с данными
В данный момент у вас уже есть классы данных и контекста, необходимые для работы с базой данных, которая еще не существует. Простое создание этих классов не приводит к автоматическому созданию (изменению) базы данных при запуске приложения. Чтобы создать базу данных, нужно добавить код работы с данными с помощью Entity Framework, например, написать код вставки данных в таблицу. Имея это в виду, ниже описаны шаги, с помощью которых мы добавим веб-форму в наш проект в которой будем взаимодействовать с базой данных:
Щелкните правой кнопкой мыши по имени проекта веб-сайта ASP. NET в окне Solution Explorer и выберите в контекстном меню команду Add —> New Item. В открывшемся диалоговом окне выберите шаблон Web Form, находящийся на вкладке Web. Назовите форму Default.aspx и нажмите кнопку Add.
Измените разметку формы на ту, которая показана в примере ниже:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ProfessorWeb.EntityFramework.Default" %> <!DOCTYPE html> <html> <head runat="server"> <title>Code-First в Entity Framework</title> </head> <body> <div class="form"> <form id="form1" runat="server" enctype="multipart/form-data"> <h3>Данные заказчика</h3> <div class="data"> <div> <label>Имя</label> <input name="name" /> </div> <div> <label>Email</label> <input name="email" /> </div> <div> <label>Возраст</label> <input name="age" /> </div> <div> <label>Фото</label> <input type="file" name="photo" /> </div> <input type="submit" value="Вставить в БД" /> </div> </form> </div> <style> .form { position: absolute; left: 50%; width: 470px; margin-left: -235px; background: #888; border-radius: 5px; top: 20px; } form { background: #fff; border-radius: 2px; margin: 5px; } .data { border-top: 1px solid #d5d5d5; padding: 10px 15px; } .data div { margin: 8px 0; } h3 { padding: 10px 15px; margin: 0; } label { min-width: 100px; display: block; float: left; } input[type="submit"] { margin-top: 10px; } </style> </body> </html>
Здесь мы просто добавляем форму, с помощью которой можно вставить данные заказчика в таблицу Customers.
Теперь нужно добавить в файл отделенного кода веб-формы Default.aspx.cs обработку данных формы:
using System; using System.Collections.Generic; using System.Linq; using System.Data.Entity; using System.Web; using System.Web.UI; using System.Web.ModelBinding; using System.IO; using CodeFirst; namespace ProfessorWeb.EntityFramework { public partial class Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (Page.IsPostBack) { Customer customer = new Customer(); // Получить данные из формы с помощью средств // привязки моделей ASP.NET IValueProvider provider = new FormValueProvider(ModelBindingExecutionContext); if (TryUpdateModel<Customer>(customer, provider)) { // Загрузить фото профиля с помощью средств .NET HttpPostedFile photo = Request.Files["photo"]; if (photo != null) { BinaryReader b = new BinaryReader(photo.InputStream); customer.Photo = b.ReadBytes((int)photo.InputStream.Length); } // В этой точке непосредственно начинается работа с Entity Framework // Создать объект контекста SampleContext context = new SampleContext(); // Вставить данные в таблицу Customers с помощью LINQ context.Customers.Add(customer); // Сохранить изменения в БД context.SaveChanges(); } } } } }
Код работы с Entity Framework в этом примере создает объект контекста SampleContext и использует его, чтобы добавить новые данные в таблицу Customers. Вызов метода SaveChanges()
сохраняет изменения в базе данных и при первой вставке данных вызов SaveChanges() создаст базу данных.Запустите это приложение, введите данные в форму и нажмите кнопку “Вставить в БД”:
После отправки формы вы можете повторить действия и вставить новых заказчиков. Обратите внимание, что при первой отправке возникает заметная задержка в ответе от сервера – именно в данной точке и создается база данных. Когда вы отправляете форму второй раз, эта задержка исчезает, т.к. данные вставляются в уже существующую базу данных.
DbContext pooling
Для повышения производительности при работе с EF нам необходимо постепенно уменьшать влияние промежуточных этапов которые мы описали ранее, уменьшая количество аллокации, повторных вычислений и по возможности делая часть вычислений наперед (pre-calculation). Microsoft предлагает
использовать пул для объектов типа DbContext
. Плюсы этого решения очевидны — переиспользование «тяжелых» объектов уменьшат давление на GC что будет заметно при интенсивной нагрузке. Также среди плюсов стоит отметить легкость в конфигурации — для настройки пулинга вам необходимо поменять лишь одну строку в конфигурации приложения, заменив вызов AddDbContext
на AddDbContextPool
в Program.cs. Ваш код доступа к данным (в нашем случае реализация IProductsRepository
) останется нетронутым. Однако стоит учитывать что ваш DbContext
по сути становится синглтоном и не должен сохранять никакого состояния между использованиями. Тем не менее если у вас возникает необходимость работать с данными scoped контекста, способ это сделать был предусмотрен и описан
разработчиками EF. Также важно предусмотреть достаточно большой размер пула, так как при превышении его размера будут создаваться новые экземпляры DbContext
.
public static void AddEfCore(this IServiceCollection services, IConfiguration config)
{
//services.AddDbContext<AdventureWorksContext>((dbContextConfig) =>
services.AddDbContextPool<AdventureWorksContext>((dbContextConfig) =>
{
dbContextConfig.UseSqlServer(config.GetConnectionString(ConnectionStringName));
});
services.AddScoped<IProductsRepository, EFCoreProductsRepository>();
}
Создание базы данных
Работа с базами данных в . NET Framework
— Entity Framework 6
— Создание базы данных
Ранее мы уже видели, как Entity Framework создает базу данных из сущностной модели с помощью Code-First, но при этом не описывали этот момент более подробно. С помощью Code-First можно настроить инициализацию базы данных. Например, в статье “Использование Code-First”
мы использовали следующие настройку:
Database.SetInitializer(
new DropCreateDatabaseIfModelChanges<SampleContext>());
Этот код говорит Code-First, что если модель данных изменилась, то нужно удалить и воссоздать базу данных с новой моделью.
Процесс инициализации базы данных состоит из двух этапов:
Во-первых, Code-First создает объектную модель в памяти, используя соглашения и настройки конфигурации, которые мы описывали ранее.
Во-вторых, база данных, которая будет использоваться для хранения данных, создается с использованием инициализатора базы данных, который был установлен (как показано в примере выше).
Инициализация базы данных
запускается, когда впервые используется объект контекста, при этом она происходит отложено, это означает что создание экземпляра контекста не достаточно, чтобы вызвать создание схемы базы данных. Чтобы инициализация произошла, вы должны создать запрос к базе данных, например выбрать для чтения коллекцию строк из таблицы или вставить новую запись.
Влияние комбинирования улучшений на производительность
Мы рассмотрели основные рекомендации по повышению производительности EF от Microsoft, разобрали механизм их работы, а также возможные накладные расходы при применении. Приведем общий список рекомендаций:
Используйте
DbContext
pooling.Используйте
AsNoTracking
для запросов только для чтения.Применяйте пре-компилированные в SQL запросы.
Отключайте проверки на потокобезопасность (помня о рисках).
Пришло время их скомбинировать и провести повторное тестирование нашей системы.
Согласно результатам тестирования всех трех версий приложения, мы видим что улучшения для EF позволили на 6-25
процентов улучшить результаты по сравнению с версией EF «из коробки». Также значительно сократился разрыв с Dapper и теперь Dapper превосходит EF в среднем на 1.5-4.2
процента в большинстве сценариев на чтение.
К сожалению, мы также увидели что получить схожую с Dapper производительность для сценариев с редактированием и созданием, при этом сохраняя изоляцию C# программиста от SQL кода, увы не выйдет. Dapper все еще превосходит EF на 76
процентов в редактировании и на 74
процента в создании. Однако стоит отметить что EF конечно же дает программисту возможность вручную писать SQL код с помощью DbContext.Database.ExecuteSqlRaw
. Таким образом вы сможете оптимизировать узкое место, не подключая при этом сторонних библиотек кроме EF. Результаты бенчмарка показывают что производительность EF ExecuteSqlRaw
почти идентична коду, написанному на Dapper для обоих сценариев:
public async Task EditProductName(int productId, string productName)
{
var rowsAffected = await _context.Database.ExecuteSqlRawAsync(@"UPDATE [Production].[Product]
SET [Name] = {0}
WHERE [ProductID] = {1}
SELECT @@ROWCOUNT", productName, productId);
...
}
public async Task<int> CreateProduct(Product product)
{
var productId = await _context.Database.ExecuteSqlRawAsync(@"INSERT INTO [Production].[Product]
(Name, ProductNumber, SafetyStockLevel, ReorderPoint, StandardCost, ListPrice, Class, Style, Color, SellStartDate, DaysToManufacture)
VALUES
({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10})
SELECT CAST(SCOPE_IDENTITY() as int)",
product.Name, product.ProductNumber, product.SafetyStockLevel, product.ReorderPoint, product.StandardCost, product.ListPrice,
product.Class, product.Style, product.Color, product.SellStartDate, product.DaysToManufacture);
return productId;
}
Стоит также добавить что в дорожной карте
для следующей версии EF планируется провести оптимизацию change-tracking механизма и улучшить производительность сценариев Insert и Update:
For EF7, we plan to focus on performance related to database inserts and updates. This includes performance of change-tracking queries, performance of
DetectChanges
, and performance of the insert and update commands sent to the database.
Мы можем следить за ходом разработки на Github
и надеяться что со следующим релизом разрыв с Dapper в этих сценариях будет существенно сокращен.
Отключение внутренних проверок потокобезопасности
DbContext
в Entity Framework Core, в отличие от версии для Framework, не поддерживает сценарии работы с несколькими потоками. Для поддержки этого ограничения в EF присутствуют внутренние проверки, которые обнаруживают доступ из нескольких потоков и с помощью понятного исключения уведомляют программиста о неправильном использовании. Однако когда ваше приложение многократно проверено в проде, вы полностью уверены, что ошибок с многопоточностью у вас нет и вы используете DbContext
правильно, стоит ли рассматривать эти проверки как накладные расходы, которые можно сократить ? Все рассмотренные выше рекомендации могут создавать определенный дискомфорт при разработке и имеют свои ограничения, однако ни одна из них не ставит под угрозу корректность и работоспособность EF. Отключение кода проверяющего корректное использование DbContext
может иметь непредсказуемые последствия, о чем прямо предупреждается в документации к EF:
WARNING
: Only disable thread safety checks after thoroughly testing that your application doesn’t contain such concurrency bugs.
Однако в контексте данной статьи и перечисления способов повышения производительности для EF стоит упомянуть что возможность отключить проверки потокобезопасности в DbContext
есть. Для этого нужно вызвать соответсвующий метод в месте вызова AddDbContext
:
public static void AddEfCore(this IServiceCollection services, IConfiguration config)
{
services.AddDbContextPool<AdventureWorksContext>(
dbContextConfig =>
{
...
dbContextConfig.EnableThreadSafetyChecks(enableChecks: false);
});
...
}
Данная конфигурация, так же как и другие, была проверена с помощью BenchmarkDotNet, однако из всех опробованных улучшений показала минимальное влияние на производительность. К сожалению, цифру в 5
процентов прироста производительности, указанную в одной из issue на Github
, мне повторить не удалось. Применять ли эту опцию в ваших продуктах — решать вам.
Создание базы данных с некоторыми данными по умолчанию
Ранее в этой статье вы видели как и когда создается база данных в Code-First и как этим процессом можно управлять. При этом создаваемая база данных всегда пуста, хотя возможны ситуации, когда вы захотите вставить некоторые начальные данные при создании базы. Например, вы можете добавить некоторые справочные таблицы, которые содержат статичную информацию — список стран, валют, список областей и населенных пунктов Российской Федерации и т.п.
Для этого вы можете использовать свой инициализатор данных, унаследованный от одного из стандартных инициализаторов и переопределить его метод Seed(). Допустим вам нужно добавить таблицу, со всеми крупными населенными пунктами России, чтобы можно было привязать адрес проживания покупателя в модели. Для этого давайте определим следующую модель:
public class Customer
{
// ...
// Город проживания покупателя
public City Location { get; set; }
[ForeignKey("Location")]
public int LocationId { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
}
В таблице City мы будем хранить список городов. Чтобы заполнить эту таблицу автоматически при инициализации базы данных, можно использовать следующий код:
using System.Collections.Generic;
using System.Data.Entity;
namespace CodeFirst
{
public class SampleInitializer
: DropCreateDatabaseIfModelChanges<SampleContext>
{
// В этом методе можно заполнить таблицу по умолчанию
protected override void Seed(SampleContext context)
{
List<City> cities = new List<City>
{
new City { Name = "Москва" },
new City { Name = "Санкт-Петербург" },
new City { Name = "Казань" }
// ...
};
foreach (City city in cities)
context.Cities.Add(city);
context.SaveChanges();
base.Seed(context);
}
}
public class SampleContext : DbContext
{
public SampleContext() : base("MyShop")
{
// Установить новый инициализатор
Database.SetInitializer<SampleContext>(new SampleInitializer());
}
public DbSet<Customer> Customers { get; set; }
public DbSet<City> Cities { get; set; }
}
}
После запуска этого примера, в базу данных будет добавлена помимо других, таблица Cities, в которую будет автоматически сохранен список городов. В этом примере мы создаем список городов вручную, хотя в реальном приложении такой список скорее всего будет загружаться через веб-службу или из файла.
Отключение отслеживания изменений в объектах для read-only запросов
Рассматривая особенности работы EF мы упоминали систему отслеживания изменений
. Change-tracking позволяет нам обновлять данные трансформируя изменения свойств объектов в SQL Update операции. Эта система включена по умолчанию для всех запросов, однако она имеет смысл только тогда, когда мы собираемся что-то редактировать. В сценариях только для чтения, эта система только создает дополнительные расходы. К счастью, ее можно отключить для конкретного запроса, вызвав метод AsNoTracking
.
public async Task<Product> GetProduct(int productId, CancellationToken cancellationToken = default)
{
return await _context.Products
.AsNoTracking()
.FirstOrDefaultAsync(x => x.ProductId == productId, cancellationToken);
}
За пару лет я завел себе привычку всегда писать запросы через AsNoTracking
, потому что запросы только для чтения приходится писать чаще чем запросы для редактирования. Однако если такой привычки у вас нет то вам необходимо будет выполнить некий объем работы, чтобы проанализировать свой код доступа к данным, выделить запросы только для чтения, добавить AsNoTracking
и провести тестирование, чтобы убедится что никакие сценарии редактирования не сломались.
Стоит также добавить что поведение запросов по умолчанию в EF можно настроить таким образом, что все запросы будут повторять поведение AsNoTracking
без явного вызова этого метода. Настроить это можно в месте вызова AddDbContext
. Тогда вам наоборот придется явно добавлять вызов метода AsTracking
в тех сценариях, где необходимо что-то отредактировать.
public static void AddEfCore(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<AdventureWorksContext>((dbContextConfig) =>
{
...
dbContextConfig.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
}
public void ApplicationLogic()
{
using var context = new AdventureWorksContext();
var bookProduct = context.Products.AsTracking().Where(p => p.Name == "Harry Potter").Single();
bookProduct.Name = "Harry Potter and the Sorcerer's Stone"
context.SaveChanges();
}