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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,11 +45,20 @@ 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
|
||||||
{
|
{
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -94,6 +94,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
README.md
82
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` | Создаёт хранимые процедуры |
|
| 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()`. Показывает, что все команды внутри транзакции получают
|
|
||||||
третий параметр `transaction`.
|
* `CreateOrderWithTransactionAsync()` — «боевой» сценарий: `BeginTransactionAsync()`
|
||||||
|
→ `CommitAsync()` / `RollbackAsync()`. Все команды внутри транзакции получают
|
||||||
|
третий параметр `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` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
-- 01_create_database.sql
|
-- 01_create_database.sql
|
||||||
-- Скрипт для создания базы данных
|
-- Скрипт для создания базы данных
|
||||||
|
|
||||||
USE master;
|
USE master;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
-- 02_create_tables.sql
|
-- 02_create_tables.sql
|
||||||
-- Скрипт для создания таблиц
|
-- Скрипт для создания таблиц
|
||||||
|
|
||||||
USE [AdoNetShop];
|
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];
|
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),
|
|
||||||
('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');
|
|
||||||
|
|
||||||
-- Заказы добавлять не будем, создадим их из кода, чтобы протестировать транзакции
|
|
||||||
GO
|
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];
|
USE [AdoNetShop];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
-- 05_procedures.sql
|
-- 05_procedures.sql
|
||||||
-- Хранимые процедуры (Stored Procedures).
|
-- Хранимые процедуры (Stored Procedures).
|
||||||
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
|
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
|
||||||
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
|
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user