Add rollback demo, idempotent seed data, UTF-8 BOM SQL scripts, RollForward, console robustness

This commit is contained in:
Student 2026-10-05 06:33:36 +05:00
parent 9abe9d3db3
commit 9eb82e44c9
11 changed files with 347 additions and 72 deletions

View File

@ -5,6 +5,9 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Проект собран под .NET 8, но при необходимости запускается на более новой
установленной версии .NET (9, 10, ...), даже если runtime 8.0 не установлен. -->
<RollForward>LatestMajor</RollForward>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -90,6 +90,120 @@ public class OrderRepository
} }
} }
// Демонстрация отката (ROLLBACK) для защиты лабораторной.
// Первая позиция списывается успешно, на второй специально выбрасывается
// ошибка. В результате БД не меняется вообще: остатки возвращаются
// к прежнему значению, а заказ не создаётся.
// Метод возвращает текст описания случившимся — для вывода в консоль.
public async Task<string> 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<string>();
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<List<Order>> GetAllOrdersBasicAsync() public async Task<List<Order>> GetAllOrdersBasicAsync()
{ {

View File

@ -1,4 +1,4 @@
using System.Data; using System.Data;
using AdoNetShop.Data; using AdoNetShop.Data;
using AdoNetShop.Models; using AdoNetShop.Models;
using AdoNetShop.Services; using AdoNetShop.Services;
@ -30,7 +30,7 @@ class Program
bool exit = false; bool exit = false;
while (!exit) while (!exit)
{ {
Console.Clear(); ClearConsole();
Console.WriteLine("================================"); Console.WriteLine("================================");
Console.WriteLine(" ADO.NET SHOP"); Console.WriteLine(" ADO.NET SHOP");
Console.WriteLine("================================"); Console.WriteLine("================================");
@ -45,12 +45,21 @@ class Program
Console.WriteLine("9. Просмотреть заказы"); Console.WriteLine("9. Просмотреть заказы");
Console.WriteLine("10. Демонстрация DataAdapter (Disconnected mode)"); Console.WriteLine("10. Демонстрация DataAdapter (Disconnected mode)");
Console.WriteLine("11. Демонстрация Stored Procedures"); Console.WriteLine("11. Демонстрация Stored Procedures");
Console.WriteLine("12. Демонстрация ROLLBACK транзакции");
Console.WriteLine("0. Выход"); Console.WriteLine("0. Выход");
Console.WriteLine("================================"); Console.WriteLine("================================");
Console.Write("Выберите действие: "); Console.Write("Выберите действие: ");
var input = Console.ReadLine(); var input = Console.ReadLine();
// Ввод закрыт (например, перенаправлен и закончился) — выходим из программы,
// иначе цикл меню будет крутиться бесконечно.
if (input is null)
{
exit = true;
continue;
}
try try
{ {
switch (input) switch (input)
@ -88,6 +97,9 @@ class Program
case "11": case "11":
await DemoStoredProcedures(shopService); await DemoStoredProcedures(shopService);
break; break;
case "12":
await DemoRollback(shopService);
break;
case "0": case "0":
exit = true; exit = true;
break; break;
@ -110,11 +122,39 @@ class Program
if (!exit) if (!exit)
{ {
Console.WriteLine("\nНажмите любую клавишу для продолжения..."); 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) private static async Task ShowAllProducts(ShopService service)
{ {
Console.WriteLine("\n--- Список товаров ---"); Console.WriteLine("\n--- Список товаров ---");
@ -343,4 +383,18 @@ class Program
? $"Цена товара [{products[0].Id}] успешно изменена на {newPrice:C}" ? $"Цена товара [{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);
}
} }

View File

@ -95,5 +95,9 @@ public class ShopService
public Task<List<Order>> GetAllOrdersAsync() => _orderRepository.GetAllOrdersBasicAsync(); public Task<List<Order>> GetAllOrdersAsync() => _orderRepository.GetAllOrdersBasicAsync();
// Демонстрация отката транзакции (пункт меню 12)
public Task<string> CreateOrderWithRollbackDemoAsync(int customerId)
=> _orderRepository.CreateOrderWithRollbackDemoAsync(customerId);
public Task<System.Data.DataTable> GetProductsTableAsync() => _productRepository.GetProductsTableAsync(); public Task<System.Data.DataTable> GetProductsTableAsync() => _productRepository.GetProductsTableAsync();
} }

View File

@ -584,9 +584,15 @@ docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Your_password123!" -p 1433:1
| 5 | `05_procedures.sql` | Создаёт хранимые процедуры | | 5 | `05_procedures.sql` | Создаёт хранимые процедуры |
| 6 | `06_test.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) ### Способ 2: через командную строку (sqlcmd)
@ -668,6 +674,26 @@ Server=(local)\SQLEXPRESS — эквивалент
Server=.\MSSQLSERVER — именованный экземпляр основного сервера 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 (логин и пароль) ### Если нужна аутентификация SQL Server (логин и пароль)
```json ```json
@ -740,15 +766,29 @@ dotnet run
4. Изменить товар 4. Изменить товар
5. Удалить товар 5. Удалить товар
6. Список категорий 6. Список категорий
7. Список клиенты 7. Список клиентов
8. Создать заказ (Демонстрация Transaction) 8. Создать заказ (Демонстрация Transaction)
9. Просмотреть заказы 9. Просмотреть заказы
10. Демонстрация DataAdapter (Disconnected mode) 10. Демонстрация DataAdapter (Disconnected mode)
11. Демонстрация Stored Procedures 11. Демонстрация Stored Procedures
12. Демонстрация ROLLBACK транзакции
0. Выход 0. Выход
================================ ================================
``` ```
> **Если .NET 8 не установлен.** Проект собирается под `net8.0`, но в `.csproj`
> включён параметр `<RollForward>LatestMajor</RollForward>`. Это значит: если на
> компьютере установлена только более новая версия .NET (9, 10, 11…),
> приложение всё равно запустится на ней. Если же ошибка всё-таки появилась,
> либо поставьте .NET 8 SDK, либо смените в `AdoNetShop.csproj` строку
> `<TargetFramework>net8.0</TargetFramework>` на установленную у вас версию.
> **Кодировка SQL-файлов.** Все скрипты в папке `database` сохранены в UTF-8 **с BOM**
> (метка порядка байтов в начале файла). Благодаря этому кириллица корректно читается
> и в SSMS, и в Visual Studio, и в командной строке (`sqlcmd -f 65001 -i файл.sql`).
> Если вы пересохраните скрипт в другой кодировке — русские названия могут превратиться
> в «кракозябры».
--- ---
## 11. Что умеет программа (демонстрация преподавателю) ## 11. Что умеет программа (демонстрация преподавателю)
@ -832,6 +872,24 @@ dotnet run
Из C# мы вызываем их по имени, а не пишем запрос текстом. Основная бизнес-логика Из C# мы вызываем их по имени, а не пишем запрос текстом. Основная бизнес-логика
при этом остаётся в приложении — процедуры здесь только для демонстрации». при этом остаётся в приложении — процедуры здесь только для демонстрации».
### Пункт 12. Демонстрация ROLLBACK → наглядный откат транзакции ★
Второй сценарий, который обязательно стоит показать преподавателю.
Метод `OrderRepository.CreateOrderWithRollbackDemoAsync()` делает ровно то,
что описано в учебнике про транзакции:
1. Открывает транзакцию (`BeginTransactionAsync`).
2. Создаёт заказ.
3. **Успешно** списывает 2 штуки первого товара.
4. Затем «падает» с ошибкой (искусственное исключение).
5. Выполняет `RollbackAsync()`.
6. Читает остатки заново и показывает их в консоли.
**Что увидит преподаватель:** первое списание прошло успешно, но после отката
остатки **такие же, как были до операции**, а заказ в базе отсутствует.
Вот эту картинку и нужно доказать: «даже если часть операций внутри транзакции
успела выполниться, откат возвращает базу в исходное состояние».
--- ---
## 12. Обработка ошибок и валидация ## 12. Обработка ошибок и валидация
@ -914,9 +972,14 @@ catch (SqlException ex)
| `UpdatePriceViaStoredProcedureAsync(...)` | `StoredProcedure` + `UPDATE`, `SqlException` | | `UpdatePriceViaStoredProcedureAsync(...)` | `StoredProcedure` + `UPDATE`, `SqlException` |
### 3. `AdoNetShop/Data/OrderRepository.cs` — транзакция ### 3. `AdoNetShop/Data/OrderRepository.cs` — транзакция
Метод `CreateOrderWithTransactionAsync()`: `BeginTransactionAsync()` → `CommitAsync()` Два метода, которые обязательно нужно показать:
/ `RollbackAsync()`. Показывает, что все команды внутри транзакции получают
* `CreateOrderWithTransactionAsync()` — «боевой» сценарий: `BeginTransactionAsync()`
→ `CommitAsync()` / `RollbackAsync()`. Все команды внутри транзакции получают
третий параметр `transaction`. третий параметр `transaction`.
* `CreateOrderWithRollbackDemoAsync()` — наглядная демонстрация отката
для защиты: часть операций успевает выполниться, потом происходит ошибка,
и `RollbackAsync()` возвращает базу в исходное состояние.
### 4. `AdoNetShop/appsettings.json` — конфигурация ### 4. `AdoNetShop/appsettings.json` — конфигурация
Строка подключения вынесена из кода. Покажите преподавателю, что в исходниках Строка подключения вынесена из кода. Покажите преподавателю, что в исходниках
@ -944,6 +1007,9 @@ catch (SqlException ex)
| `The model ... context has been changed` | Изменили модель после миграции | Не применимо к этому проекту — здесь **нет** EF и миграций | | `The model ... context has been changed` | Изменили модель после миграции | Не применимо к этому проекту — здесь **нет** EF и миграций |
| Ошибка `PK_...` при `INSERT` | Попытка вставить дубликат | Например, два клиента с одинаковым Email (`UNIQUE`) | | Ошибка `PK_...` при `INSERT` | Попытка вставить дубликат | Например, два клиента с одинаковым Email (`UNIQUE`) |
| Не компилируется проект | Не установлен .NET 8 SDK | Установите SDK и выполните `dotnet restore` | | Не компилируется проект | Не установлен .NET 8 SDK | Установите SDK и выполните `dotnet restore` |
| `You must install or update .NET to run this application` | На компьютере нет runtime .NET 8 | Установите .NET 8 SDK **или** проверьте, что в `.csproj` есть `<RollForward>LatestMajor</RollForward>` |
| Вместо русских букв «кракозябры» | Скрипт сохранён не в 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` | | 10 | `SqlTransaction`, `Commit`, `Rollback` | `Data/OrderRepository.cs` |
| 11 | `CommandType.StoredProcedure` | `GetProductsByStoredProcedureAsync` | | 11 | `CommandType.StoredProcedure` | `GetProductsByStoredProcedureAsync` |
| 12 | `SqlException` | `UpdatePriceViaStoredProcedureAsync` | | 12 | `SqlException` | `UpdatePriceViaStoredProcedureAsync` |
| 13 | `Commit` / `Rollback` | `CreateOrderWithTransactionAsync` |
| 14 | Наглядный откат (отмена выполненных операций) | `CreateOrderWithRollbackDemoAsync` |
--- ---

View File

@ -1,4 +1,4 @@
-- 01_create_database.sql -- 01_create_database.sql
-- Скрипт для создания базы данных -- Скрипт для создания базы данных
USE master; USE master;

View File

@ -1,4 +1,4 @@
-- 02_create_tables.sql -- 02_create_tables.sql
-- Скрипт для создания таблиц -- Скрипт для создания таблиц
USE [AdoNetShop]; USE [AdoNetShop];

View File

@ -1,42 +1,66 @@
-- 03_insert_data.sql -- 03_insert_data.sql
-- Скрипт для вставки тестовых данных -- Тестовые данные для интернет-магазина.
--
-- Скрипт можно запускать сколько угодно раз: данные каждый раз
-- подставляются заново и получают одни и те же Id (1, 2, 3, ...).
-- Для этого Id задаются явно (SET IDENTITY_INSERT), а в конце счётчики
-- IDENTITY приводятся к правильному состоянию.
USE [AdoNetShop]; USE [AdoNetShop];
GO GO
-- Очистка таблиц перед вставкой (опционально, для чистоты эксперимента) -- Удаляем старые данные. Порядок важен: сначала удаляем таблицы,
-- Отключаем проверку внешних ключей -- на которые ссылаются другие (иначе нарушится FOREIGN KEY).
EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL' DELETE FROM [OrderItems];
EXEC sp_MSForEachTable 'DELETE FROM ?' DELETE FROM [Orders];
EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL' DELETE FROM [Products];
-- Сброс identity DELETE FROM [Categories];
DBCC CHECKIDENT ('Categories', RESEED, 0); DELETE FROM [Customers];
DBCC CHECKIDENT ('Products', RESEED, 0);
DBCC CHECKIDENT ('Customers', RESEED, 0);
DBCC CHECKIDENT ('Orders', RESEED, 0);
DBCC CHECKIDENT ('OrderItems', RESEED, 0);
GO GO
-- ============ Категории ============
-- Добавляем категории -- SET IDENTITY_INSERT ON позволяет указать значение колонки IDENTITY вручную.
INSERT INTO Categories (Name) VALUES -- В каждый момент времени это разрешено только для одной таблицы,
('Laptops'), -- поэтому ON и OFF ставятся вокруг каждого INSERT.
('Smartphones'), SET IDENTITY_INSERT [Categories] ON;
('Accessories'); INSERT INTO [Categories] (Id, Name) VALUES
(1, N'Ноутбуки'),
-- Добавляем товары (2, N'Смартфоны'),
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId) VALUES (3, N'Аксессуары');
('Laptop Dell XPS 15', 'High performance laptop', 1500.00, 10, 1), SET IDENTITY_INSERT [Categories] OFF;
('MacBook Pro 14', 'Apple laptop with M2 chip', 1999.99, 5, 1), GO
('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), SET IDENTITY_INSERT [Products] ON;
('USB-C Hub', 'Multi-port adapter', 45.00, 30, 3); 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),
INSERT INTO Customers (Name, Email, Phone) VALUES (3, N'iPhone 14 Pro', N'Флагманский смартфон Apple', 999.00, 20, 2),
('Ivan Ivanov', 'ivan@example.com', '+1234567890'), (4, N'Samsung Galaxy S23', N'Флагманский смартфон Samsung', 899.50, 15, 2),
('Petr Petrov', 'petr@example.com', '+0987654321'); (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 GO

View File

@ -1,4 +1,4 @@
-- 04_queries.sql -- 04_queries.sql
-- Примеры запросов -- Примеры запросов
USE [AdoNetShop]; USE [AdoNetShop];

View File

@ -1,4 +1,4 @@
-- 05_procedures.sql -- 05_procedures.sql
-- Хранимые процедуры (Stored Procedures). -- Хранимые процедуры (Stored Procedures).
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET). -- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
-- Здесь только простые процедуры, чтобы показать, как ADO.NET -- Здесь только простые процедуры, чтобы показать, как ADO.NET

View File

@ -1,5 +1,7 @@
-- 06_test.sql -- 06_test.sql
-- Проверочные запросы. Выполняйте вручную после 01-05 скриптов. -- Проверочные запросы: выполните вручную после скриптов 01-05.
-- Скрипт ничего не ломает: он только читает данные и показывает,
-- какие ошибки ожидаются при нарушении ограничений.
USE [AdoNetShop]; USE [AdoNetShop];
GO GO
@ -11,21 +13,23 @@ SELECT * FROM Customers;
SELECT * FROM Orders; SELECT * FROM Orders;
SELECT * FROM OrderItems; SELECT * FROM OrderItems;
-- ============ Примеры SELECT-запросов из приложения ============ -- ============ Примеры запросов, которые выполняет приложение ============
-- 1. Все товары (демонстрация SqlDataReader) -- 1. Все товары (демонстрация SqlDataReader)
SELECT Id, Name, Description, Price, StockQuantity, CategoryId SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products; FROM Products;
GO GO
-- 2. Один товар по Id (демонстрация параметризованного SqlCommand) -- 2. Один товар по Id (демонстрация параметризованного SqlCommand).
DECLARE @Id INT = 1; -- Id берём из таблицы, чтобы скрипт работал после любого числа перезапусков.
DECLARE @Id INT = (SELECT MIN(Id) FROM Products);
SELECT Id, Name, Description, Price, StockQuantity, CategoryId SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products FROM Products
WHERE Id = @Id; WHERE Id = @Id;
GO GO
-- 3. Товары с названием категории (JOIN) -- 3. Товары вместе с названиями категорий (JOIN)
SELECT p.Id, p.Name, p.Price, p.StockQuantity, c.Name AS CategoryName SELECT p.Id, p.Name, p.Price, p.StockQuantity, c.Name AS CategoryName
FROM Products p FROM Products p
JOIN Categories c ON p.CategoryId = c.Id JOIN Categories c ON p.CategoryId = c.Id
@ -38,37 +42,41 @@ GO
EXEC dbo.GetProducts; EXEC dbo.GetProducts;
GO GO
-- Товары только 1-й категории -- Товары только первой категории
EXEC dbo.GetProducts @CategoryId = 1; DECLARE @CatId INT = (SELECT MIN(Id) FROM Categories);
EXEC dbo.GetProducts @CategoryId = @CatId;
GO GO
-- Один товар -- Один товар
EXEC dbo.GetProductById @ProductId = 1; DECLARE @Id2 INT = (SELECT MIN(Id) FROM Products);
EXEC dbo.GetProductById @ProductId = @Id2;
GO GO
-- Успешное изменение цены -- Успешное изменение цены
EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = 1450.00; DECLARE @Id3 INT = (SELECT MIN(Id) FROM Products);
SELECT Id, Name, Price FROM Products WHERE Id = 1; EXEC dbo.UpdateProductPrice @ProductId = @Id3, @NewPrice = 1450.00;
SELECT Id, Name, Price FROM Products WHERE Id = @Id3;
GO GO
-- Ожидаемая ошибка: цена <= 0 (процедура сама вызовет RAISERROR) -- Ожидается ошибка: цена должна быть > 0 (RAISERROR внутри процедуры).
EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = -10.00; -- Снимите комментарий со следующей строки, чтобы увидеть ошибку:
-- EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = -10.00;
GO GO
-- ============ Проверка ограничений (CHECK) ============ -- ============ Проверка ограничений (CHECK) ============
-- Ожидаемая ошибка: Price > 0 -- Ожидается ошибка CHECK: Price > 0
INSERT INTO Products (Name, Price, StockQuantity, CategoryId) -- INSERT INTO Products (Name, Price, StockQuantity, CategoryId)
VALUES ('Тестовый товар', -5, 1, 1); -- VALUES (N'Тестовый товар', -5, 1, 1);
GO GO
-- Ожидаемая ошибка: StockQuantity >= 0 -- Ожидается ошибка CHECK: StockQuantity >= 0
UPDATE Products SET StockQuantity = -1 WHERE Id = 1; -- UPDATE Products SET StockQuantity = -1 WHERE Id = 1;
GO GO
-- ============ Проверка целостности связей (FOREIGN KEY) ============ -- ============ Проверка целостности связей (FOREIGN KEY) ============
-- Ожидаемая ошибка: категории 999 не существует -- Ожидается ошибка FOREIGN KEY: категории 999 не существует
INSERT INTO Products (Name, Price, StockQuantity, CategoryId) -- INSERT INTO Products (Name, Price, StockQuantity, CategoryId)
VALUES ('Товар без категории', 100, 1, 999); -- VALUES (N'Товар без категории', 100, 1, 999);
GO GO