Add rollback demo, idempotent seed data, UTF-8 BOM SQL scripts, RollForward, console robustness
This commit is contained in:
parent
9abe9d3db3
commit
9eb82e44c9
|
|
@ -5,6 +5,9 @@
|
|||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Проект собран под .NET 8, но при необходимости запускается на более новой
|
||||
установленной версии .NET (9, 10, ...), даже если runtime 8.0 не установлен. -->
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,12 +45,21 @@ 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();
|
||||
|
||||
// Ввод закрыт (например, перенаправлен и закончился) — выходим из программы,
|
||||
// иначе цикл меню будет крутиться бесконечно.
|
||||
if (input is null)
|
||||
{
|
||||
exit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (input)
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,5 +95,9 @@ public class ShopService
|
|||
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
80
README.md
80
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`
|
||||
> включён параметр `<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. Что умеет программа (демонстрация преподавателю)
|
||||
|
|
@ -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()`. Показывает, что все команды внутри транзакции получают
|
||||
Два метода, которые обязательно нужно показать:
|
||||
|
||||
* `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` есть `<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` |
|
||||
| 11 | `CommandType.StoredProcedure` | `GetProductsByStoredProcedureAsync` |
|
||||
| 12 | `SqlException` | `UpdatePriceViaStoredProcedureAsync` |
|
||||
| 13 | `Commit` / `Rollback` | `CreateOrderWithTransactionAsync` |
|
||||
| 14 | Наглядный откат (отмена выполненных операций) | `CreateOrderWithRollbackDemoAsync` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- 01_create_database.sql
|
||||
-- 01_create_database.sql
|
||||
-- Скрипт для создания базы данных
|
||||
|
||||
USE master;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- 02_create_tables.sql
|
||||
-- 02_create_tables.sql
|
||||
-- Скрипт для создания таблиц
|
||||
|
||||
USE [AdoNetShop];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
-- 04_queries.sql
|
||||
-- 04_queries.sql
|
||||
-- Примеры запросов
|
||||
|
||||
USE [AdoNetShop];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- 05_procedures.sql
|
||||
-- 05_procedures.sql
|
||||
-- Хранимые процедуры (Stored Procedures).
|
||||
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
|
||||
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user