diff --git a/AdoNetShop/AdoNetShop.csproj b/AdoNetShop/AdoNetShop.csproj index b68cac9..678b460 100644 --- a/AdoNetShop/AdoNetShop.csproj +++ b/AdoNetShop/AdoNetShop.csproj @@ -5,6 +5,9 @@ net8.0 enable enable + + LatestMajor diff --git a/AdoNetShop/Data/OrderRepository.cs b/AdoNetShop/Data/OrderRepository.cs index 578dcd3..69a9a81 100644 --- a/AdoNetShop/Data/OrderRepository.cs +++ b/AdoNetShop/Data/OrderRepository.cs @@ -90,6 +90,120 @@ public class OrderRepository } } + // Демонстрация отката (ROLLBACK) для защиты лабораторной. + // Первая позиция списывается успешно, на второй специально выбрасывается + // ошибка. В результате БД не меняется вообще: остатки возвращаются + // к прежнему значению, а заказ не создаётся. + // Метод возвращает текст описания случившимся — для вывода в консоль. + public async Task CreateOrderWithRollbackDemoAsync(int customerId) + { + await using var connection = _connectionFactory.CreateConnection(); + await connection.OpenAsync(); + + // Берём два товара, которые есть на складе + string selectSql = "SELECT TOP 2 Id, Name, Price, StockQuantity FROM Products WHERE StockQuantity >= 2 ORDER BY Id"; + var demoItems = new List<(int Id, string Name, decimal Price, int Stock)>(); + + await using (var selectCommand = new SqlCommand(selectSql, connection)) + await using (var reader = await selectCommand.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + demoItems.Add(( + reader.GetInt32(0), + reader.GetString(1), + reader.GetDecimal(2), + reader.GetInt32(3))); + } + } + + if (demoItems.Count < 2) + return "Для демонстрации нужно минимум 2 товара с остатком от 2 штук."; + + var report = new List(); + report.Add("Остатки ДО операции:"); + foreach (var it in demoItems) + report.Add($" [{it.Id}] {it.Name} — было {it.Stock} шт."); + + await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync(); + + try + { + // Шаг 1. Создаём заказ (он тоже будет откатан) + string insertOrder = @" + INSERT INTO Orders (CustomerId, OrderDate, Status, TotalAmount) + VALUES (@CustomerId, @OrderDate, @Status, @TotalAmount);"; + + decimal total = 0m; + foreach (var it in demoItems) total += it.Price * 2; + + await using (var orderCommand = new SqlCommand(insertOrder, connection, transaction)) + { + orderCommand.Parameters.Add("@CustomerId", SqlDbType.Int).Value = customerId; + orderCommand.Parameters.Add("@OrderDate", SqlDbType.DateTime2).Value = DateTime.Now; + orderCommand.Parameters.Add("@Status", SqlDbType.NVarChar, 50).Value = "Created"; + orderCommand.Parameters.Add("@TotalAmount", SqlDbType.Decimal).Value = total; + await orderCommand.ExecuteNonQueryAsync(); + } + + // Шаг 2. Списываем первую позицию — это успешно + await DeductStockAsync(connection, transaction, demoItems[0].Id, 2); + + report.Add($"Списание товара [{demoItems[0].Id}] {demoItems[0].Name} - 2 шт. прошло успешно."); + report.Add("Но тут приложение «падает» (искусственная ошибка)."); + report.Add("Вызывается Rollback — все изменения выше отменяются:"); + + // Шаг 3. Искусственная ошибка внутри транзакции + throw new InvalidOperationException("Искусственная ошибка при обработке второй позиции заказа"); + } + catch (Exception ex) + { + // Откат: и заказ, и списание первой позиции исчезают + await transaction.RollbackAsync(); + report.Add($"Ошибка: {ex.Message}"); + report.Add("Выполнен transaction.RollbackAsync()."); + } + + // Читаем остатки ПОСЛЕ отката — база вернулась в исходное состояние + string checkSql = @" + SELECT Id, Name, StockQuantity + FROM Products + WHERE Id = @Id1 OR Id = @Id2"; + + await using var checkCommand = new SqlCommand(checkSql, connection); + checkCommand.Parameters.Add("@Id1", SqlDbType.Int).Value = demoItems[0].Id; + checkCommand.Parameters.Add("@Id2", SqlDbType.Int).Value = demoItems[1].Id; + + report.Add("Остатки ПОСЛЕ операции:"); + await using (var checkReader = await checkCommand.ExecuteReaderAsync()) + { + while (await checkReader.ReadAsync()) + { + report.Add($" [{checkReader.GetInt32(0)}] {checkReader.GetString(1)} — стало {checkReader.GetInt32(2)} шт."); + } + } + + report.Add("Транзакция откатилась: остатки не изменились, заказ в базе отсутствует."); + return string.Join(Environment.NewLine, report); + } + + // Уменьшение остатка товара внутри транзакции + private static async Task DeductStockAsync(SqlConnection connection, SqlTransaction transaction, int productId, int quantity) + { + string updateSql = @" + UPDATE Products + SET StockQuantity = StockQuantity - @Quantity + WHERE Id = @ProductId AND StockQuantity >= @Quantity"; + + await using var command = new SqlCommand(updateSql, connection, transaction); + command.Parameters.Add("@ProductId", SqlDbType.Int).Value = productId; + command.Parameters.Add("@Quantity", SqlDbType.Int).Value = quantity; + + int affected = await command.ExecuteNonQueryAsync(); + if (affected == 0) + throw new InvalidOperationException($"Недостаточно товара на складе (Id = {productId})"); + } + // Получение списка заказов (простая реализация) public async Task> GetAllOrdersBasicAsync() { diff --git a/AdoNetShop/Program.cs b/AdoNetShop/Program.cs index 7d2b76a..9b0445c 100644 --- a/AdoNetShop/Program.cs +++ b/AdoNetShop/Program.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using AdoNetShop.Data; using AdoNetShop.Models; using AdoNetShop.Services; @@ -30,7 +30,7 @@ class Program bool exit = false; while (!exit) { - Console.Clear(); + ClearConsole(); Console.WriteLine("================================"); Console.WriteLine(" ADO.NET SHOP"); Console.WriteLine("================================"); @@ -45,11 +45,20 @@ class Program Console.WriteLine("9. Просмотреть заказы"); Console.WriteLine("10. Демонстрация DataAdapter (Disconnected mode)"); Console.WriteLine("11. Демонстрация Stored Procedures"); + Console.WriteLine("12. Демонстрация ROLLBACK транзакции"); Console.WriteLine("0. Выход"); Console.WriteLine("================================"); Console.Write("Выберите действие: "); - var input = Console.ReadLine(); +var input = Console.ReadLine(); + + // Ввод закрыт (например, перенаправлен и закончился) — выходим из программы, + // иначе цикл меню будет крутиться бесконечно. + if (input is null) + { + exit = true; + continue; + } try { @@ -88,6 +97,9 @@ class Program case "11": await DemoStoredProcedures(shopService); break; + case "12": + await DemoRollback(shopService); + break; case "0": exit = true; break; @@ -110,11 +122,39 @@ class Program if (!exit) { Console.WriteLine("\nНажмите любую клавишу для продолжения..."); - Console.ReadKey(); + WaitForAnyKey(); } } } + // Очистка экрана. Console.Clear() не работает, если вывод перенаправлен + // (например, при запуске через管道 или в CI) — в этом случае просто пропускаем её. + private static void ClearConsole() + { + try + { + Console.Clear(); + } + catch (IOException) + { + // Консоли нет — ничего не очищаем + } + } + + // Console.ReadKey() тоже требует настоящую консоль. + // Если ввод перенаправлен, просто читаем строку ввода. + private static void WaitForAnyKey() + { + if (Console.IsInputRedirected) + { + Console.ReadLine(); + } + else + { + Console.ReadKey(); + } + } + private static async Task ShowAllProducts(ShopService service) { Console.WriteLine("\n--- Список товаров ---"); @@ -343,4 +383,18 @@ class Program ? $"Цена товара [{products[0].Id}] успешно изменена на {newPrice:C}" : "Цена не была изменена (смотрите сообщение об ошибке выше)."); } -} + + // Наглядная демонстрация отката транзакции (Rollback). + // Первая позиция списывается успешно, затем происходит ошибка — + // и вся транзакция отменяется. Остатки в базе не меняются. + private static async Task DemoRollback(ShopService service) + { + Console.WriteLine("\n--- Демонстрация ROLLBACK транзакции ---"); + Console.Write("Введите Id клиента (Enter = 1): "); + string input = Console.ReadLine() ?? ""; + int customerId = int.TryParse(input, out int id) && id > 0 ? id : 1; + + string report = await service.CreateOrderWithRollbackDemoAsync(customerId); + Console.WriteLine(report); + } +} \ No newline at end of file diff --git a/AdoNetShop/Services/ShopService.cs b/AdoNetShop/Services/ShopService.cs index b68da47..a123721 100644 --- a/AdoNetShop/Services/ShopService.cs +++ b/AdoNetShop/Services/ShopService.cs @@ -94,6 +94,10 @@ public class ShopService } public Task> GetAllOrdersAsync() => _orderRepository.GetAllOrdersBasicAsync(); + + // Демонстрация отката транзакции (пункт меню 12) + public Task CreateOrderWithRollbackDemoAsync(int customerId) + => _orderRepository.CreateOrderWithRollbackDemoAsync(customerId); public Task GetProductsTableAsync() => _productRepository.GetProductsTableAsync(); } diff --git a/README.md b/README.md index cf0f0b5..627d8e1 100644 --- a/README.md +++ b/README.md @@ -584,9 +584,15 @@ docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Your_password123!" -p 1433:1 | 5 | `05_procedures.sql` | Создаёт хранимые процедуры | | 6 | `06_test.sql` | Проверочные запросы (для самопроверки, можно не выполнять) | -Скрипты написаны «безопасно»: `01` использует `IF NOT EXISTS`, `02` создаёт -таблицы только если их нет, `05` удаляет процедуру перед созданием. Поэтому -скрипты можно запускать повторно — ошибки «уже существует» не будет. +Скрипты написаны «безопасно» и их можно запускать повторно: + +* `01` создаёт базу только если её ещё нет; +* `02` создаёт таблицы только если их нет; +* `03` сначала удаляет прежние данные, а затем подставляет свои значения вместе с + `Id` (через `SET IDENTITY_INSERT`), поэтому результат всегда одинаковый: + товары `Id = 1…6`, клиенты `Id = 1…2`; +* `05` удаляет процедуру перед созданием, поэтому повторный запуск не вызовет ошибку + «процедура уже существует». ### Способ 2: через командную строку (sqlcmd) @@ -668,6 +674,26 @@ Server=(local)\SQLEXPRESS — эквивалент Server=.\MSSQLSERVER — именованный экземпляр основного сервера ``` +### Если используется LocalDB (лёгкая версия SQL Server для разработки) + +LocalDB — это «облегчённый» SQL Server, который запускается одной командой и не требует +установки службы. Он удобен тем, что не занимает ресурсы и не мешает другим программам. + +Запуск экземпляра (по умолчанию он называется `MSSQLLocalDB`): + +```bash +sqllocaldb start MSSQLLocalDB +``` + +Строка подключения для него: + +```json +"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;" +``` + +> В файле `appsettings.json` строка по умолчанию — `Server=localhost`. +> Если у вас LocalDB, замените её на вариант выше. + ### Если нужна аутентификация SQL Server (логин и пароль) ```json @@ -740,15 +766,29 @@ dotnet run 4. Изменить товар 5. Удалить товар 6. Список категорий -7. Список клиенты +7. Список клиентов 8. Создать заказ (Демонстрация Transaction) 9. Просмотреть заказы 10. Демонстрация DataAdapter (Disconnected mode) 11. Демонстрация Stored Procedures +12. Демонстрация ROLLBACK транзакции 0. Выход ================================ ``` +> **Если .NET 8 не установлен.** Проект собирается под `net8.0`, но в `.csproj` +> включён параметр `LatestMajor`. Это значит: если на +> компьютере установлена только более новая версия .NET (9, 10, 11…), +> приложение всё равно запустится на ней. Если же ошибка всё-таки появилась, +> либо поставьте .NET 8 SDK, либо смените в `AdoNetShop.csproj` строку +> `net8.0` на установленную у вас версию. + +> **Кодировка SQL-файлов.** Все скрипты в папке `database` сохранены в UTF-8 **с BOM** +> (метка порядка байтов в начале файла). Благодаря этому кириллица корректно читается +> и в SSMS, и в Visual Studio, и в командной строке (`sqlcmd -f 65001 -i файл.sql`). +> Если вы пересохраните скрипт в другой кодировке — русские названия могут превратиться +> в «кракозябры». + --- ## 11. Что умеет программа (демонстрация преподавателю) @@ -832,6 +872,24 @@ dotnet run Из C# мы вызываем их по имени, а не пишем запрос текстом. Основная бизнес-логика при этом остаётся в приложении — процедуры здесь только для демонстрации». +### Пункт 12. Демонстрация ROLLBACK → наглядный откат транзакции ★ + +Второй сценарий, который обязательно стоит показать преподавателю. +Метод `OrderRepository.CreateOrderWithRollbackDemoAsync()` делает ровно то, +что описано в учебнике про транзакции: + +1. Открывает транзакцию (`BeginTransactionAsync`). +2. Создаёт заказ. +3. **Успешно** списывает 2 штуки первого товара. +4. Затем «падает» с ошибкой (искусственное исключение). +5. Выполняет `RollbackAsync()`. +6. Читает остатки заново и показывает их в консоли. + +**Что увидит преподаватель:** первое списание прошло успешно, но после отката +остатки **такие же, как были до операции**, а заказ в базе отсутствует. +Вот эту картинку и нужно доказать: «даже если часть операций внутри транзакции +успела выполниться, откат возвращает базу в исходное состояние». + --- ## 12. Обработка ошибок и валидация @@ -914,9 +972,14 @@ catch (SqlException ex) | `UpdatePriceViaStoredProcedureAsync(...)` | `StoredProcedure` + `UPDATE`, `SqlException` | ### 3. `AdoNetShop/Data/OrderRepository.cs` — транзакция -Метод `CreateOrderWithTransactionAsync()`: `BeginTransactionAsync()` → `CommitAsync()` -/ `RollbackAsync()`. Показывает, что все команды внутри транзакции получают -третий параметр `transaction`. +Два метода, которые обязательно нужно показать: + +* `CreateOrderWithTransactionAsync()` — «боевой» сценарий: `BeginTransactionAsync()` + → `CommitAsync()` / `RollbackAsync()`. Все команды внутри транзакции получают + третий параметр `transaction`. +* `CreateOrderWithRollbackDemoAsync()` — наглядная демонстрация отката + для защиты: часть операций успевает выполниться, потом происходит ошибка, + и `RollbackAsync()` возвращает базу в исходное состояние. ### 4. `AdoNetShop/appsettings.json` — конфигурация Строка подключения вынесена из кода. Покажите преподавателю, что в исходниках @@ -944,6 +1007,9 @@ catch (SqlException ex) | `The model ... context has been changed` | Изменили модель после миграции | Не применимо к этому проекту — здесь **нет** EF и миграций | | Ошибка `PK_...` при `INSERT` | Попытка вставить дубликат | Например, два клиента с одинаковым Email (`UNIQUE`) | | Не компилируется проект | Не установлен .NET 8 SDK | Установите SDK и выполните `dotnet restore` | +| `You must install or update .NET to run this application` | На компьютере нет runtime .NET 8 | Установите .NET 8 SDK **или** проверьте, что в `.csproj` есть `LatestMajor` | +| Вместо русских букв «кракозябры» | Скрипт сохранён не в UTF-8 | Не пересохраняйте `.sql` в ANSI-кодировке либо запускайте `sqlcmd -f 65001 -i файл.sql` | +| Ошибка FOREIGN KEY при повторном запуске `03_insert_data.sql` | Ручная вставка данных без `IDENTITY_INSERT` | Запустите скрипт заново: он сам удаляет старые данные и подставляет свои Id | --- @@ -963,6 +1029,8 @@ catch (SqlException ex) | 10 | `SqlTransaction`, `Commit`, `Rollback` | `Data/OrderRepository.cs` | | 11 | `CommandType.StoredProcedure` | `GetProductsByStoredProcedureAsync` | | 12 | `SqlException` | `UpdatePriceViaStoredProcedureAsync` | +| 13 | `Commit` / `Rollback` | `CreateOrderWithTransactionAsync` | +| 14 | Наглядный откат (отмена выполненных операций) | `CreateOrderWithRollbackDemoAsync` | --- diff --git a/database/01_create_database.sql b/database/01_create_database.sql index 8b94a39..26c4cf5 100644 --- a/database/01_create_database.sql +++ b/database/01_create_database.sql @@ -1,4 +1,4 @@ --- 01_create_database.sql +-- 01_create_database.sql -- Скрипт для создания базы данных USE master; diff --git a/database/02_create_tables.sql b/database/02_create_tables.sql index f5e7cbf..570ba79 100644 --- a/database/02_create_tables.sql +++ b/database/02_create_tables.sql @@ -1,4 +1,4 @@ --- 02_create_tables.sql +-- 02_create_tables.sql -- Скрипт для создания таблиц USE [AdoNetShop]; diff --git a/database/03_insert_data.sql b/database/03_insert_data.sql index 1886288..6a88c4e 100644 --- a/database/03_insert_data.sql +++ b/database/03_insert_data.sql @@ -1,42 +1,66 @@ --- 03_insert_data.sql --- Скрипт для вставки тестовых данных +-- 03_insert_data.sql +-- Тестовые данные для интернет-магазина. +-- +-- Скрипт можно запускать сколько угодно раз: данные каждый раз +-- подставляются заново и получают одни и те же Id (1, 2, 3, ...). +-- Для этого Id задаются явно (SET IDENTITY_INSERT), а в конце счётчики +-- IDENTITY приводятся к правильному состоянию. USE [AdoNetShop]; GO --- Очистка таблиц перед вставкой (опционально, для чистоты эксперимента) --- Отключаем проверку внешних ключей -EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' -EXEC sp_MSForEachTable 'DELETE FROM ?' -EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' --- Сброс identity -DBCC CHECKIDENT ('Categories', RESEED, 0); -DBCC CHECKIDENT ('Products', RESEED, 0); -DBCC CHECKIDENT ('Customers', RESEED, 0); -DBCC CHECKIDENT ('Orders', RESEED, 0); -DBCC CHECKIDENT ('OrderItems', RESEED, 0); +-- Удаляем старые данные. Порядок важен: сначала удаляем таблицы, +-- на которые ссылаются другие (иначе нарушится FOREIGN KEY). +DELETE FROM [OrderItems]; +DELETE FROM [Orders]; +DELETE FROM [Products]; +DELETE FROM [Categories]; +DELETE FROM [Customers]; GO - --- Добавляем категории -INSERT INTO Categories (Name) VALUES -('Laptops'), -('Smartphones'), -('Accessories'); - --- Добавляем товары -INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId) VALUES -('Laptop Dell XPS 15', 'High performance laptop', 1500.00, 10, 1), -('MacBook Pro 14', 'Apple laptop with M2 chip', 1999.99, 5, 1), -('iPhone 14 Pro', 'Latest Apple smartphone', 999.00, 20, 2), -('Samsung Galaxy S23', 'Flagship Android phone', 899.50, 15, 2), -('Wireless Mouse', 'Ergonomic wireless mouse', 25.00, 50, 3), -('USB-C Hub', 'Multi-port adapter', 45.00, 30, 3); - --- Добавляем клиентов -INSERT INTO Customers (Name, Email, Phone) VALUES -('Ivan Ivanov', 'ivan@example.com', '+1234567890'), -('Petr Petrov', 'petr@example.com', '+0987654321'); - --- Заказы добавлять не будем, создадим их из кода, чтобы протестировать транзакции +-- ============ Категории ============ +-- SET IDENTITY_INSERT ON позволяет указать значение колонки IDENTITY вручную. +-- В каждый момент времени это разрешено только для одной таблицы, +-- поэтому ON и OFF ставятся вокруг каждого INSERT. +SET IDENTITY_INSERT [Categories] ON; +INSERT INTO [Categories] (Id, Name) VALUES +(1, N'Ноутбуки'), +(2, N'Смартфоны'), +(3, N'Аксессуары'); +SET IDENTITY_INSERT [Categories] OFF; GO + +-- ============ Товары ============ +SET IDENTITY_INSERT [Products] ON; +INSERT INTO [Products] (Id, Name, Description, Price, StockQuantity, CategoryId) VALUES +(1, N'Ноутбук Dell XPS 15', N'Мощный ноутбук для работы и игр', 1500.00, 10, 1), +(2, N'MacBook Pro 14', N'Ноутбук Apple на чипе M2 Pro', 1999.99, 5, 1), +(3, N'iPhone 14 Pro', N'Флагманский смартфон Apple', 999.00, 20, 2), +(4, N'Samsung Galaxy S23', N'Флагманский смартфон Samsung', 899.50, 15, 2), +(5, N'Беспроводная мышь', N'Эргономичная беспроводная мышь', 25.00, 50, 3), +(6, N'USB-C хаб', N'Многопортовый адаптер USB-C', 45.00, 30, 3); +SET IDENTITY_INSERT [Products] OFF; +GO + +-- ============ Клиенты ============ +SET IDENTITY_INSERT [Customers] ON; +INSERT INTO [Customers] (Id, Name, Email, Phone) VALUES +(1, N'Иван Иванов', N'ivan@example.com', N'+7 999 123-45-67'), +(2, N'Пётр Петров', N'petr@example.com', N'+7 999 765-43-21'); +SET IDENTITY_INSERT [Customers] OFF; +GO + +-- ============ Заказы ============ +-- Заказы намеренно НЕ создаются скриптом: их удобнее оформить +-- из приложения (пункт меню 8), чтобы наглядно показать работу транзакции. +GO + +-- Приводим счётчики IDENTITY к текущему максимуму, чтобы следующий +-- товар/клиент/заказ, созданный из приложения, получил свободный Id. +-- (Например, первым заказом из приложения будет Id = 1.) +DBCC CHECKIDENT ('Categories', RESEED, 3) WITH NO_INFOMSGS; +DBCC CHECKIDENT ('Products', RESEED, 6) WITH NO_INFOMSGS; +DBCC CHECKIDENT ('Customers', RESEED, 2) WITH NO_INFOMSGS; +DBCC CHECKIDENT ('Orders', RESEED, 0) WITH NO_INFOMSGS; +DBCC CHECKIDENT ('OrderItems', RESEED, 0) WITH NO_INFOMSGS; +GO \ No newline at end of file diff --git a/database/04_queries.sql b/database/04_queries.sql index 87563f7..fedf2db 100644 --- a/database/04_queries.sql +++ b/database/04_queries.sql @@ -1,4 +1,4 @@ --- 04_queries.sql +-- 04_queries.sql -- Примеры запросов USE [AdoNetShop]; diff --git a/database/05_procedures.sql b/database/05_procedures.sql index 0daedd4..5c217eb 100644 --- a/database/05_procedures.sql +++ b/database/05_procedures.sql @@ -1,4 +1,4 @@ --- 05_procedures.sql +-- 05_procedures.sql -- Хранимые процедуры (Stored Procedures). -- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET). -- Здесь только простые процедуры, чтобы показать, как ADO.NET diff --git a/database/06_test.sql b/database/06_test.sql index d97a7d2..594030a 100644 --- a/database/06_test.sql +++ b/database/06_test.sql @@ -1,5 +1,7 @@ --- 06_test.sql --- Проверочные запросы. Выполняйте вручную после 01-05 скриптов. +-- 06_test.sql +-- Проверочные запросы: выполните вручную после скриптов 01-05. +-- Скрипт ничего не ломает: он только читает данные и показывает, +-- какие ошибки ожидаются при нарушении ограничений. USE [AdoNetShop]; GO @@ -11,21 +13,23 @@ SELECT * FROM Customers; SELECT * FROM Orders; SELECT * FROM OrderItems; --- ============ Примеры SELECT-запросов из приложения ============ +-- ============ Примеры запросов, которые выполняет приложение ============ -- 1. Все товары (демонстрация SqlDataReader) SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products; GO --- 2. Один товар по Id (демонстрация параметризованного SqlCommand) -DECLARE @Id INT = 1; +-- 2. Один товар по Id (демонстрация параметризованного SqlCommand). +-- Id берём из таблицы, чтобы скрипт работал после любого числа перезапусков. +DECLARE @Id INT = (SELECT MIN(Id) FROM Products); + SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products WHERE Id = @Id; GO --- 3. Товары с названием категории (JOIN) +-- 3. Товары вместе с названиями категорий (JOIN) SELECT p.Id, p.Name, p.Price, p.StockQuantity, c.Name AS CategoryName FROM Products p JOIN Categories c ON p.CategoryId = c.Id @@ -38,37 +42,41 @@ GO EXEC dbo.GetProducts; GO --- Товары только 1-й категории -EXEC dbo.GetProducts @CategoryId = 1; +-- Товары только первой категории +DECLARE @CatId INT = (SELECT MIN(Id) FROM Categories); +EXEC dbo.GetProducts @CategoryId = @CatId; GO -- Один товар -EXEC dbo.GetProductById @ProductId = 1; +DECLARE @Id2 INT = (SELECT MIN(Id) FROM Products); +EXEC dbo.GetProductById @ProductId = @Id2; GO -- Успешное изменение цены -EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = 1450.00; -SELECT Id, Name, Price FROM Products WHERE Id = 1; +DECLARE @Id3 INT = (SELECT MIN(Id) FROM Products); +EXEC dbo.UpdateProductPrice @ProductId = @Id3, @NewPrice = 1450.00; +SELECT Id, Name, Price FROM Products WHERE Id = @Id3; GO --- Ожидаемая ошибка: цена <= 0 (процедура сама вызовет RAISERROR) -EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = -10.00; +-- Ожидается ошибка: цена должна быть > 0 (RAISERROR внутри процедуры). +-- Снимите комментарий со следующей строки, чтобы увидеть ошибку: +-- EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = -10.00; GO -- ============ Проверка ограничений (CHECK) ============ --- Ожидаемая ошибка: Price > 0 -INSERT INTO Products (Name, Price, StockQuantity, CategoryId) -VALUES ('Тестовый товар', -5, 1, 1); +-- Ожидается ошибка CHECK: Price > 0 +-- INSERT INTO Products (Name, Price, StockQuantity, CategoryId) +-- VALUES (N'Тестовый товар', -5, 1, 1); GO --- Ожидаемая ошибка: StockQuantity >= 0 -UPDATE Products SET StockQuantity = -1 WHERE Id = 1; +-- Ожидается ошибка CHECK: StockQuantity >= 0 +-- UPDATE Products SET StockQuantity = -1 WHERE Id = 1; GO -- ============ Проверка целостности связей (FOREIGN KEY) ============ --- Ожидаемая ошибка: категории 999 не существует -INSERT INTO Products (Name, Price, StockQuantity, CategoryId) -VALUES ('Товар без категории', 100, 1, 999); +-- Ожидается ошибка FOREIGN KEY: категории 999 не существует +-- INSERT INTO Products (Name, Price, StockQuantity, CategoryId) +-- VALUES (N'Товар без категории', 100, 1, 999); GO \ No newline at end of file