[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
В этой статье, мы продолжим писать магазин на Asp.Net Core и Entity Framework Core. В третьей части мы расширили магазин за счет добавления новых классов и создания отношений между ними. Узнали, как запрашивать связанные данные, каким образом выполнять обновления. Для ознакомления с третьей частью, перейдите по ссылке.
При создании приложения основное внимание обычно уделяется построению правильного фундамента и как раз такой подход был принят в проекте GameStore. По мере развития приложения зачастую полезно увеличивать объем данных, с которыми производится работа, чтобы можно было видеть, какое влияние они оказывают на операции, инициируемые пользователем, и на время, требующееся для их выполнения. В этой главе в БД будут добавлены тестовые данные, чтобы продемонстрировать недостатки способа, которым приложение представляет данные пользователю, и принять соответствующие меры за счет добавления поддержки разбиения на страницы, упорядочения и поиска в данных. Также будет показано, каким образом улучшить производительность операций с использованием инфраструктуры Entity Framework Core, которая поддерживает расширенные варианты конфигурации модели данных, известные как интерфейс Fluent АРI.
Магазин на Asp.Net Core MVC EF. Часть 4
Создание контроллера и представления для начального заполнения данными
Нам необходим контроллер, который сможет заполнять БД тестовыми данными. Добавьте в папку Controller, новый контролер SeedController, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class SeedController : Controller { private readonly ApplicationContext _context; public SeedController(ApplicationContext context) { _context = context; } public IActionResult Index() { ViewBag.Count = _context.Products.Count(); return View(_context.Products.Include(e => e.Category).OrderBy(e => e.Id).Take(20)); } [HttpPost] public IActionResult CreateSeedData(int count) { ClearData(); if (count > 0) { _context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); _context.Database.ExecuteSqlRaw("DROP PROCEDURE IF EXISTS CreateSeedData"); _context.Database.ExecuteSqlRaw($@" CREATE PROCEDURE CreateSeedData @RowCount decimal AS BEGIN SET NOCOUNT ON DECLARE @i INT = 0; DECLARE @catId INT; DECLARE @CatCount INT = @RowCount / 10; DECLARE @pprice DECIMAL(5,2); DECLARE @rprice DECIMAL(5,2); BEGIN TRANSACTION WHILE @i < @CatCount BEGIN INSERT INTO Categories (Name,Description) VALUES (CONCAT('Category-',@i), 'Test Data Category'); SET @catId = SCOPE_IDENTITY(); DECLARE @j INT = 1; WHILE @j <= 10 BEGIN SET @pprice = RAND() * (500-5+1); SET @rprice = (RAND() * @pprice) + @pprice; INSERT INTO Products (Name,CategoryId,PurchasePrice,RetailPrice) VALUES (CONCAT('Product',@i,'-',@j),@catId,@pprice,@rprice) SET @j = @j + 1 END SET @i = @i + 1 END COMMIT END"); _context.Database.BeginTransaction(); _context.Database.ExecuteSqlRaw($"EXEC CreateSeedData @RowCount = {count}"); _context.Database.CommitTransaction(); } return RedirectToAction(nameof(Index)); } [HttpPost] public IActionResult ClearData() { _context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); _context.Database.BeginTransaction(); _context.Database.ExecuteSqlRaw("DELETE FROM Orders"); _context.Database.ExecuteSqlRaw("DELETE FROM Categories"); _context.Database.CommitTransaction(); return RedirectToAction(nameof(Index)); } } |
Когда дело доходит до генерации крупных объемов тестовых данных, то создание объектов .NET и их сохранение в БД будет неэффективным. Контроллер Seed применяет средства Entity Framework Core для работы напрямую с SQL. чтобы создать и выполнить хранимую процедуру, которая производит тестовые данные гораздо быстрее.
Не поступайте так в реальных проектах
Данный подход, должен использоваться только для генерации тестовых данных и ни в какой другой части приложения. В этой главе нужен механизм, который позволяет надежно генерировать крупные объемы тестовых данных, не требуя выполнения сложных задач в БД или применения сторонних инструментов.
При работе непосредственно с SQL следует соблюдать осторожность, поскольку в таком случае обходятся многочисленные полезные средства защиты, обеспечиваемые инфраструктурой Entity Framework Core. Итоговый код SQL трудно тестировать и сопровождать, к тому же зачастую оказывается, что он работает на единственном сервере баз данных. Короче говоря, не используйте никакие аспекты продемонстрированного приема в производственных частях своих приложений.
Что бы снабдить контроллер представлением, создайте папку Views / Seed и поместите в нее файл по имени Index.cshtml, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
@{ ViewData["Title"] = "Все заказы"; } @model IEnumerable<Product> <h3 class="p-2 bg-primary text-white text-center">Начальные данные</h3> <form method="post"> <div class="form-group"> <label>Количество объектов для создания:</label> <input class="form-control" name="count" value="50" /> </div> <div class="text-center"> <button type="submit" asp-action="CreateSeedData" class="btn btn-primary">Заполнить базу</button> <button type="submit" asp-action="ClearData" class="btn btn-primary">Очистить базу</button> </div> </form> <h5>Всего @ViewBag.Count товаров в Базе Данных</h5> <div class="container-fluid"> <div class="row"> <div class="col-1 fw-bold">Id</div> <div class="col fw-bold">Название</div> <div class="col fw-bold">Категория</div> <div class="col fw-bold">Закупка</div> <div class="col fw-bold">Продажа</div> </div> @foreach (Product product in Model) { <div class="row"> <div class="col-1">@product.Id</div> <div class="col">@product.Name</div> <div class="col">@product.Category.Name</div> <div class="col">@product.PurchasePrice</div> <div class="col">@product.RetailPrice</div> </div> } </div> |
Представление позволяет указывать, сколько тестовых данных должно быть сгенерировано, и отображает первые 20 объектов Product, которые предоставляются запросом в действии Index контроллера Seed. Чтобы облегчить работу с контроллером Seed, добавим ссылку в меню _Layout.cshtml:
1 2 3 |
<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Seed" asp-action="Index">Начальные данные</a> </li> |
Запустите приложение, перейдите на страницу Seed/Index.cshtml, выполните очистку базы (если хотите), нажав кнопку “Очистить базу”. Теперь укажите значение 500 в элементе input, и нажмите кнопку “Заполнить базу”. Генерация данных займет какое-то время, после чего вы увидите результат:
Масштабирование представления данных
Чтобы продемонстрировать изъяны способа, которым приложение GameStore представляет свои данные, особо много данных не требуется. При наличии тысячи объектов метод представления данных пользователю становится непригодным, и это все еще относительно небольшой объем данных, с которым приложению приходится иметь дело. В последующих разделах способ представления данных приложением GameStore будет изменен, чтобы помочь пользователю выполнять основные операции и находить интересующие его объекты.
Добавление поддержки разбиения на страницы
Первой будет решена задача разбиения данных, представляемых пользователю, с тем, чтобы они не выглядели как один длинный список. Использование простых таблиц, которые включают все объекты, является удобным подходом, когда формируются основы приложения, но таблицы, содержащие тысячи строк, непригодны для применения в большинстве приложений. Чтобы решить эту проблему, мы добавим поддержку запрашивания у БД небольших объемов данных и позволим пользователю перемещаться, перелистывая такие страницы с небольшими объемами данных.
При работе с крупными объемами данных важно обеспечить согласованное управление доступом к этим данным, чтобы у одной части приложения не было никакой возможности случайно запросить миллионы объектов. Мы примем подход, предусматривающий создание класса коллекции, который заключает в себе разбиение на страницы.
Для определения коллекции, которая обеспечит доступ к разбитым на страницы данным, создадим папку Models / Pages, добавим в нее файл класс по имени PagedList.cs, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PagedList<T>:List<T> { public PagedList(IQueryable<T> query, QueryOptions options = null) { CurrentPage = options.CurrentPage; PageSize = options.PageSize; TotalPages = query.Count() / PageSize; AddRange(query.Skip((CurrentPage - 1) * PageSize).Take(PageSize)); } public int CurrentPage { get; set; } public int PageSize { get; set; } public int TotalPages { get; set; } public bool HasPreviousPage => CurrentPage > 1; public bool HasNextPage => CurrentPage < TotalPages; } |
В качестве базового класса используется строго типизированный List. который позволит легко наращивать базовое поведение коллекции. Конструктор принимает объект реализации IQueryable<T>, представляющий запрос, который будет снабжать данными для отображения пользователю. Такой запрос будет выполняться дважды — один раз для получения общего количества объектов, которое мог бы возвратить запрос, и один раз для получения только объектов, подлежащих отображению на текущей странице. Это компромисс, присущий разбиению на страницы, при котором дополнительный запрос COUNT уравновешивается запросами для меньшего количества объектов в целом. В остальных аргументах конструктора указываются страница, требуемая для запроса, и количество объектов. отображаемых на странице.
Для представления параметров, необходимых запросу, добавьте в папку Models / Pages файл класса по имени QueryOptions.cs со следующим содержимым:
1 2 3 4 5 |
public class QueryOptions { public int CurrentPage { get; set; } = 1; public int PageSize { get; set; } = 10; } |
Обновление хранилища
Чтобы обеспечить согласованное применение средства разбиения на страницы, в качестве результата выполняемых в хранилище запросов будет возвращаться объект PagedList. Добавьте в интерфейс IProduct, метод по имени GetProduct(), который возвращает данные для одиночной страницы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public interface IProduct { PagedList<Product> GetProducts(QueryOptions options); IEnumerable<Product> GetAllProducts(); Product GetProduct(int id); void AddProduct(Product product); void UpdateProduct(Product product); void UpdateAll(Product[] products); void DeleteProduct(Product product); } Внесите соответствующие изменения в класс реализации хранилища ProductRepository: public class ProductRepository : IProduct { //... public PagedList<Product> GetProducts(QueryOptions options) { return new PagedList<Product>(_context.Products.Include(e => e.Category), options); } } |
Новый метод возвращает коллекцию PagedList объектов Product для страницы, указанной аргументами.
Обновление контроллера и представления
Для добавления в контроллер Home поддержки разбиения на страницы измените, действие Index, чтобы оно принимало аргументы, необходимые при выборе страницы, и в результате использовало новый метод хранилища:
1 2 3 4 5 6 7 8 9 |
public class HomeController : Controller { //... [HttpGet] public IActionResult Index(QueryOptions options) { return View(_products.GetProducts(options)); } } |
Базовый класс коллекции данных, разбитых на страницы, реализует интерфейс IEnumerable<T>, который сводит к минимуму объем изменений, требующихся для поддержки разбитых на страницы данных. Единственное изменение, которое понадобится внести в представление для действия Index контроллера Home, связано с отображением частичного представления с деталями разбиения на страницы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@{ ViewData["Title"] = "Все товары"; } @model IEnumerable<Product> <h3 class="p-2 bg-primary text-white text-center">Товары</h3> <div class="text-center"> @Html.Partial("Pages", Model) </div> <div class="container"> <div class="row"> <div class="col fw-bold">Название</div> <div class="col fw-bold">Категория</div> <div class="col fw-bold">Закупочная цена</div> <div class="col fw-bold">Розничная цена</div> <div class="col"></div> </div> @foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category.Name</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <form asp-action="DeleteProduct" method="post"> <input type="hidden" name="Id" value="@product.Id"> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class="btn btn-outline-primary">Редактировать</a> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> </div> </div> } <div class="text-cente r р-2"> <a asp-action="UpdateProduct" asp-route-id="0" class="btn btn-primary">Добавить</a> </div> </div> |
Чтобы завершить поддержку разбиения на страницы для объектов Product, определите частичное представление, добавив в папку Views / Shared файл по имени Pages.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<form method="get" class="form-inline m-3" id="pageform"> <button name="options.CurrentPage" value="@(Model.CurrentPage - 1)" class="btn btn-outline-primary @(!Model.HasPreviousPage ? "disabled" : "")" type="submit"> Назад </button> @for (int i = 1; i <= 3 && i <= Model.TotalPages; i++) { <button name="options.CurrentPage" value="@i" type="submit" class="btn btn-outline-primary @(Model.CurrentPage == i ? "active" : "")"> @i </button> } @if (Model.CurrentPage > 3 && Model.TotalPages - Model.CurrentPage >= 3) { @:... <button class="btn btn-outline-primary active">@Model.CurrentPage</button> } @if (Model.TotalPages > 3) { @:... @for (int i = Math.Max(4, Model.TotalPages - 2); i <= Model.TotalPages; i++) { <button name="options.CurrentPage" value="@i" type="submit" class="btn btn-outline-primary @(Model.CurrentPage == i ? "active" : "")"> @i </button> } } <button name="options.CurrentPage" value="@(Model.CurrentPage + 1)" type="submit" class="btn btn-outline-primary @(!Model.HasNextPage ? "disabled":"")"> Вперед </button> <select name="options.PageSize" class="my-form-control"> @foreach (int val in new int[] { 10, 25, 50, 100 }) { <option value="@val" selected="@(Model.PageSize == val)">@val</option> } </select> <input type="hidden" name="options.CurrentPage" value="1" /> <button type="submit" class="btn btn-outline-primary">Изменить размер страницы</button> </form> <style> .my-form-control { padding: 0.375rem 0.75rem; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; background-color: #fff; background-clip: padding-box; border: 1px solid #ced4da; -webkit-appearance: none; -moz-appearance: none; appearance: none; border-radius: 0.25rem; transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; } </style> |
Представление содержит НТМL-форму, которая применяется для отправки НТТР запросов GET обратно методу действия страниц данных и для изменения размера страницы. Выражения Razor выглядят запутанными, но они приспосабливают кнопки страниц, отображаемые пользователю, к имеющемуся количеству страниц.
Так же обязательно добавьте к <form>, id=”pageform”, он нам пригодится в последующем частичном представлении.
При запуске приложения, список товаров будет разбит на 10 элементов, по которым можно переходить с помощью последовательности кнопок:
Выполните переходы постранично, измените, количество страниц для отображения, проверьте работу приложения.
Добавление поддержки поиска и упорядочения
Отображение страниц — хорошее начало, но концентрироваться на специфическом наборе объектов по-прежнему трудно. Для оснащения пользователя инструментами нахождения интересующих данных мы добавим к разбиению на страницы поддержку изменения порядка отображения и выполнения поиска. Отправной точкой будет расширение класса PagedList, чтобы он мог выполнять поиск и упорядочивать результаты запросов с использованием имен свойств, а не лямбда-выражений, выбирающих свойства.
Выполнение таких операций потребует несколько запутанного кода, но результат может быть применен к любому классу модели данных и легче интегрируется с частями ASP.NET Соге MVC приложения.
Изменим определение класса PagedList следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class PagedList<T> : List<T> { public PagedList(IQueryable<T> query, QueryOptions options = null) { CurrentPage = options.CurrentPage; PageSize = options.PageSize; Options = options; if (options != null) { if (!string.IsNullOrEmpty(options.OrderPropertyName)) { query = Order(query, options.OrderPropertyName, options.DescendingOrder); } if (!string.IsNullOrEmpty(options.SearchPropertyName) && !string.IsNullOrEmpty(options.SearchTerm)) { query = Search(query, options.SearchPropertyName, options.SearchTerm); } } TotalPages = query.Count() / PageSize; AddRange(query.Skip((CurrentPage - 1) * PageSize).Take(PageSize)); } public int CurrentPage { get; set; } public int PageSize { get; set; } public int TotalPages { get; set; } public QueryOptions Options { get; set; } public bool HasPreviousPage => CurrentPage > 1; public bool HasNextPage => CurrentPage < TotalPages; private static IQueryable<T> Search(IQueryable<T> query, string propertyName, string searchTerm) { var parameter = Expression.Parameter(typeof(T), "x"); var source = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property); var body = Expression.Call(source, "Contains", Type.EmptyTypes, Expression.Constant(searchTerm, typeof(string))); var lambda = Expression.Lambda<Func<T, bool>>(body, parameter); return query.Where(lambda); } private static IQueryable<T> Order(IQueryable<T> query, string propertyName, bool desc) { var parameter = Expression.Parameter(typeof(T), "x"); var source = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property); var lambda = Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(T), source.Type), source, parameter); return typeof(Queryable).GetMethods().Single(e => e.Name == (desc ? "OrderByDescending" : "OrderBy") && e.IsGenericMethodDefinition && e.GetGenericArguments().Length == 2 && e.GetParameters().Length == 2) .MakeGenericMethod(typeof(T), source.Type) .Invoke(null, new object[] { query, lambda }) as IQueryable<T>; } } |
Перейдем в класс QueryOptions, и изменим его содержимое следующим образом:
1 2 3 4 5 6 7 8 9 10 |
public class QueryOptions { public int CurrentPage { get; set; } = 1; public int PageSize { get; set; } = 10; public string OrderPropertyName { get; set; } public bool DescendingOrder { get; set; } public string SearchPropertyName { get; set; } public string SearchTerm { get; set; } } |
Чтобы создать обобщенное представление, которое будет предлагать пользователю возможности поиска и упорядочения, добавим в папку Views / Shared файл по имени PageOptions.cshtml, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<div class="container-fluid mt-2"> <div class="row m-1"> <div class="col"></div> <div class="col-1"> <label class="col-from-label">Поиск</label> </div> <div class="col-3"> <select form="pageform" name="options.searchpropertyname" class="form-control"> @foreach (string s in ViewBag.searches as string[]) { <option value="@s" selected="@(Model.Options.SearchPropertyName == s)"> @(s.IndexOf('.') == -1 ? s : s.Substring(0,s.IndexOf('.'))) </option> } </select> </div> <div class="col"> <input form="pageform" class="form-control" name="options.searchterm" value="@Model.Options.SearchTerm" /> </div> <div class="col-1 text-right"> <button form="pageform" class="btn btn-secondary" type="submit">Поиск</button> </div> <div class="col"></div> </div> <div class="row m-1"> <div class="col"></div> <div class="col-1"> <label class="col-form-label">Сортировка</label> </div> <div class="col-3"> <select form="pageform" name="options.OrderPropertyName" class="form-control"> @foreach (string s in ViewBag.sorts as string[]) { <option value="@s" selected="@(Model.Options.OrderPropertyName == s)"> @(s.IndexOf('.') == -1 ? s : s.Substring(0,s.IndexOf('.'))) </option> } </select> </div> <div class="col btn-check form-check-inline"> <input form="pageform" type="checkbox" name="Options.DescendingOrder" id="Options.DescendingOrder" class="form-check-input" value="true" checked="@Model.Options.DescendingOrder" /> <label class="form-check-label">Сортировка по убыванию</label> </div> <div class="col-1 text-right"> <button form="pageform" class="btn btn-secondary" type="submit">Сортировка</button> </div> <div class="col"></div> </div> </div> |
Представление опирается на возможность HTML 5 ассоциировать с формами элементы, определенные за пределами элемента form. Это позволяет расширить форму, которая определена в представлении Pages, элементами, специфичными для поиска и упорядочения.
Жестко кодировать список свойств, которые пользователь может задействовать для поиска или упорядочения данных в представлении, нежелательно, поэтому в целях простоты такие значения получаются из ViewBag. Решение не считается идеальным, но оно обеспечивает высокую гибкость и позволяет легко адаптировать одно и то же содержимое к разным представлениям и данным.
Чтобы отобразить пользователю элементы, связанные с поиском и упорядочением, рядом со списком объектов Product, добавим в представление Index, используемое контроллером Ноmе, следующее содержимое:
1 2 3 4 5 6 7 8 9 |
<div class="text-center"> @Html.Partial("Pages", Model) @{ ViewBag.searches = new string[] { "Name", "Category.Name" }; ViewBag.sorts = new string[] { "Name", "Category.Name", "PurchasePrice", "RetailPrice" }; } @Html.Partial("PageOptions", Model) </div> |
Блок кода указывает свойства класса Product, с помощью которых пользователь получит возможность искать и упорядочивать объекты Product, а выражение @Html.Partial визуализирует элементы для таких средств.
Применение возможностей представления данных к категориям
Процесс размещения средств для разбиения на страницы, поиска и сортировки по своим местам был непростым, но теперь, когда фундамент готов, их можно применить к другим типам данных в приложении, таким как объекты Category.
Первым делом обновим интерфейс ICategory и классы реализации, добавив метод, который принимает объект QueryOptions и возвращает результат PagedList:
1 2 3 4 5 6 7 8 |
public interface ICategory { PagedList<Category> GetCategories(QueryOptions options); IEnumerable<Category> GetAllCategories(); void AddCategory(Category category); void UpdateCategory(Category category); void DeleteCategory(Category category); } |
Теперь сам класс CategoryRepository:
1 2 3 4 5 6 7 8 |
public class CategoryRepository : ICategory { //... public PagedList<Category> GetCategories(QueryOptions options) { return new PagedList<Category>(_context.Categories, options); } } |
Добавим параметр QueryOptions к действию Index контроллера CategoriesController, который управляет объектами Category, и используем его для запрашивания хранилища:
1 2 3 4 5 6 7 8 |
public class CategoriesController : Controller { //... public IActionResult Index(QueryOptions options) { return View(_categories.GetCategories(options)); } } |
Наконец, предоставим пользователю возможность работы со средствами, добавив элементы в представление Index, которое применяется контроллером CategoriesController:
1 2 3 4 5 6 7 8 9 |
<div class="text-center"> @Html.Partial("Pages", Model) @{ ViewBag.searches = new string[] { "Name", "Description" }; ViewBag.sorts = new string[] { "Name", "Description" }; } @Html.Partial("PageOptions", Model) </div> |
Запустите приложение, перейдите на страницу Категорий. На странице присутствует список категорий, а пользователь может выполнять в них поиск и упорядочение. Проверьте все сами:
Индексация базы данных
Добавления в БД тысячи тестовых объектов оказалось достаточно для демонстрации ограничений способа, которым данные представлялись пользователю, но такого объема данных не хватит, чтобы выявить ограничения самой БД. Для выяснения эффекта от работы с крупными объемами данных добавим к конструктору PagedList операторы, которые измеряют длительность выполнения запроса и выводя затраченное время на консоль. Перейдем в класс PagedList и дополним конструктор следующим кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ CurrentPage = options.CurrentPage; PageSize = options.PageSize; Options = options; if (options != null) { if (!string.IsNullOrEmpty(options.OrderPropertyName)) { query = Order(query, options.OrderPropertyName, options.DescendingOrder); } if (!string.IsNullOrEmpty(options.SearchPropertyName) && !string.IsNullOrEmpty(options.SearchTerm)) { query = Search(query, options.SearchPropertyName, options.SearchTerm); } } Stopwatch stopwatch = Stopwatch.StartNew(); Console.Clear(); TotalPages = query.Count() / PageSize; AddRange(query.Skip((CurrentPage - 1) * PageSize).Take(PageSize)); Console.WriteLine($"Время выполнения: {stopwatch.ElapsedMilliseconds} миллисекунд."); } |
Запустите приложение, перейдите на страницу “Начальные данные” и заполните БД нужным количеством объектов. После этого, перейдите на страницу товаров, выберите свойство PurchasePrice для сортировки и выполните ее.
Если вы просмотрите журнальные сообщения, сгенерированные приложением, то заметите запросы, которые применялись для получения данных, а также время их выполнения:
1 2 3 4 5 6 7 8 9 10 11 |
SELECT[t].[Id], [t].[CategoryId], [t].[Name], [t].[PurchasePrice], [t].[RetailPrice], [c].[Id], [c].[Description], [c].[Name] FROM ( SELECT[p].[Id], [p].[CategoryId], [p].[Name], [p].[PurchasePrice], [p].[RetailPrice] FROM [Products] AS [p] ORDER BY [p].[PurchasePrice] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY ) AS[t] INNER JOIN[Categories] AS [c] ON[t].[CategoryId] = [c].[Id] ORDER BY[t].[PurchasePrice] Время выполнения: 83 миллисекунд. |
В следующей таблице приведены показатели времени, которое заняло выполнение этих запросов на моей машине разработки, для разных объемов начальных данных. Вы можете получить другие показатели, но важно отметить, что с ростом объема данных они увеличиваются.
Количество объектов | Время |
1000 | 86 мс |
10 000 | 104 мс |
100 000 | 314 мс |
1 000 000 | 2578 мс |
2 000 000 | 5156 мс |
Создание и применение индексов
Одна из проблем с производительностью возникает из-за того, что серверу баз данных приходится исследовать много строк данных в поиске данных, необходимых приложению. Эффективный способ сокращения объема работы, выполняемой сервером баз данных, предусматривает создание индексов, которые ускоряют запросы, но требуют определенного времени на первоначальную подготовку и небольшой дополнительной работы после каждого обновления. Что касается приложения GameStore, то мы добавим индексы для свойств классов Product и Category, которые пользователь сможет использовать при поиске или упорядочении данных.
Создадим индексы в классе ApplicationContext контекста БД, для этого добавим метод OnModelCreating как показано в следующем коде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> context) : base(context) { } public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderLine> OrderLines { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().HasIndex(e => e.Name); modelBuilder.Entity<Product>().HasIndex(e => e.PurchasePrice); modelBuilder.Entity<Product>().HasIndex(e => e.RetailPrice); modelBuilder.Entity<Category>().HasIndex(e => e.Name); modelBuilder.Entity<Category>().HasIndex(e => e.Description); } } |
Метод OnModelCreating() переопределен для настройки модели данных с применением средства Fluent API из Entity Framework Core. Интерфейс Fluent API позволяет переопределять стандартные линии поведения Eпtity Framework Core и получать доступ к расширенным возможностям, таким как создание индексов. В данном случае создаются индексы для свойств Name, PurchasePrice и RetailPrice класса Product, а также для свойств Name и Description класса Category.
Создавать индексы для свойств первичных или внешних ключей не нужно, поскольку инфраструктура Eпtity Framework Core по умолчанию создает их самостоятельно.
Создание индексов требует создания новой миграции и ее применения к БД.
Для создания миграции в окне Package Manager Console введите следующую команду:
1 |
Add-Migration AddIndexes |
Для выполнения инструкций миграции, в окне Package Manager Console выполните команду:
1 |
Update-Database |
Инфраструктура Entity Fгamework Core подключится к серверу баз данных, указанному в строке подключения, и выполнит операторы в миграции
P.S. При наличии в БД большого объема данных применение миграции, создающей индексы, может занять некоторое время, т.к. все существующие данные должны быть добавлены в индекс. Перед выполнением команд миграции возможно у вас возникнет желание воспользоваться контроллером Seed для сокращения объема тестовых данных.
После того, как миграция применена, перезапустите приложение и повторите выполненные ранее тестовые запросы, чтобы посмотреть, как индексы повлияли на производительность. В следующей таблице приведены показатели времени, которое заняло выполнение этих запросов с индексами на моей машине разработки, для разных объемов начальных данных.
Количество объектов | Время |
1000 | 48 мс |
10 000 | 61 мс |
100 000 | 104 мс |
1 000 000 | 287 мс |
2 000 000 | 301 мс |
Итог
В статье было показано, каким образом адаптировать приложение GameStore для работы с более крупными объемами данных. Мы добавили к нему поддержку разбиения на страницы, упорядочения и поиска данных, что позволяет пользователю иметь дело с управляемым количеством объектов за раз. Кроме того, с помощью Fluent АРI была настроена модель данных и добавлены индексы для улучшения производительности запросов. В следующей главе к приложению GameStore будет добавлен интерфейс для покупателей.
На этом статья «Магазин на Asp.Net Core MVC EF», подошла к концу, надеюсь вам было интересно. Вы можете скачать исходный код в моем репозитории — Github.
Поделитесь вашим опытом в комментариях, какой был ваш первый проект, магазин на Asp.Net Core MVC EF или что-то другое?
Так же вам может быть интересна предыдущая статья:
Вы хотите научится писать код на языке программирования C#?
Создавать различные информационные системы, состоящие из сайтов, мобильных клиентов, десктопных приложений, телеграмм-ботов и т.д.
Переходите к нам на страницу Dijix и ознакомьтесь с условиями обучения, мы специализируемся только на индивидуальных занятиях, как для начинающих, так и для более продвинутых программистов. Вы можете взять как одно занятие для проработки интересующего Вас вопроса, так и несколько, для более плотной работы. Благодаря личному кабинету, каждый студент повысит качество своего обучения, в вашем распоряжении:
- Доступ к пройденному материалу
- Тематические статьи
- Библиотека книг
- Онлайн тестирование
- Общение в закрытых группах
Живи в своем мире, программируй в нашем.