[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на 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 Сore 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 дає змогу перевизначати стандартні лінії поведінки Entity Framework Core і отримувати доступ до розширених можливостей, таких як створення індексів. У цьому випадку створюються індекси для властивостей Name, PurchasePrice і RetailPrice класу Product, а також для властивостей Name і Description класу Category.
Створювати індекси для властивостей первинних або зовнішніх ключів не потрібно, оскільки інфраструктура Еntity 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 і ознайомтеся з умовами навчання, ми спеціалізуємося тільки на індивідуальних заняттях, як для початківців, так і для просунутих програмістів. Ви можете взяти як одне заняття для опрацювання питання, що вас цікавить, так і кілька, для більш щільної роботи. Завдяки особистому кабінету, кожен студент підвищить якість свого навчання, у вашому розпорядженні:
- Доступ до пройденого матеріалу
- Тематичні статті
- Бібліотека книг
- Онлайн тестування
- Спілкування в закритих групах
Отличная вышла статья. Я с Удовольствием сначало прочитал, а после решил воплотить своими ручками. Да, столкнулся с некоторыми ошибками, не без этого. А в целом Спасибо Вам, за такую проделанную работу.
Дуже дякую!