232 lines
12 KiB
C#
232 lines
12 KiB
C#
using System.Data;
|
||
using AdoNetShop.Models;
|
||
using Microsoft.Data.SqlClient;
|
||
|
||
namespace AdoNetShop.Data;
|
||
|
||
public class OrderRepository
|
||
{
|
||
private readonly DbConnectionFactory _connectionFactory;
|
||
|
||
public OrderRepository(DbConnectionFactory connectionFactory)
|
||
{
|
||
_connectionFactory = connectionFactory;
|
||
}
|
||
|
||
// Демонстрация транзакции (Создание заказа)
|
||
public async Task<int> CreateOrderWithTransactionAsync(Order order)
|
||
{
|
||
await using var connection = _connectionFactory.CreateConnection();
|
||
await connection.OpenAsync();
|
||
|
||
// 1. Начинаем транзакцию
|
||
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
|
||
|
||
try
|
||
{
|
||
// 2. Создаем сам заказ
|
||
string insertOrderQuery = @"
|
||
INSERT INTO Orders (CustomerId, OrderDate, Status, TotalAmount)
|
||
VALUES (@CustomerId, @OrderDate, @Status, @TotalAmount);
|
||
SELECT SCOPE_IDENTITY();";
|
||
|
||
await using var insertOrderCommand = new SqlCommand(insertOrderQuery, connection, transaction);
|
||
insertOrderCommand.Parameters.Add("@CustomerId", SqlDbType.Int).Value = order.CustomerId;
|
||
insertOrderCommand.Parameters.Add("@OrderDate", SqlDbType.DateTime2).Value = order.OrderDate;
|
||
insertOrderCommand.Parameters.Add("@Status", SqlDbType.NVarChar, 50).Value = order.Status;
|
||
insertOrderCommand.Parameters.Add("@TotalAmount", SqlDbType.Decimal).Value = order.TotalAmount;
|
||
|
||
var newOrderIdObj = await insertOrderCommand.ExecuteScalarAsync();
|
||
if (newOrderIdObj == null) throw new Exception("Failed to create order.");
|
||
int newOrderId = Convert.ToInt32(newOrderIdObj);
|
||
|
||
// 3. Добавляем товары заказа и обновляем остатки
|
||
string insertItemQuery = @"
|
||
INSERT INTO OrderItems (OrderId, ProductId, Quantity, Price)
|
||
VALUES (@OrderId, @ProductId, @Quantity, @Price)";
|
||
|
||
string updateStockQuery = @"
|
||
UPDATE Products
|
||
SET StockQuantity = StockQuantity - @Quantity
|
||
WHERE Id = @ProductId AND StockQuantity >= @Quantity";
|
||
|
||
foreach (var item in order.Items)
|
||
{
|
||
// Проверка наличия и уменьшение остатка
|
||
await using var updateStockCommand = new SqlCommand(updateStockQuery, connection, transaction);
|
||
updateStockCommand.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
|
||
updateStockCommand.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
|
||
|
||
int rowsAffected = await updateStockCommand.ExecuteNonQueryAsync();
|
||
if (rowsAffected == 0)
|
||
{
|
||
// Если обновление не удалось (недостаточно товара или неверный ID),
|
||
// генерируем исключение. Это приведет к переходу в блок catch и откату транзакции.
|
||
throw new Exception($"Недостаточно товара на складе для ProductId: {item.ProductId}");
|
||
}
|
||
|
||
// Добавление позиции заказа
|
||
await using var insertItemCommand = new SqlCommand(insertItemQuery, connection, transaction);
|
||
insertItemCommand.Parameters.Add("@OrderId", SqlDbType.Int).Value = newOrderId;
|
||
insertItemCommand.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
|
||
insertItemCommand.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
|
||
insertItemCommand.Parameters.Add("@Price", SqlDbType.Decimal).Value = item.Price;
|
||
|
||
await insertItemCommand.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
// 4. Если всё прошло успешно, фиксируем транзакцию
|
||
await transaction.CommitAsync();
|
||
return newOrderId;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 5. При возникновении любой ошибки откатываем транзакцию
|
||
// Все изменения (созданный заказ, измененные остатки) будут отменены
|
||
await transaction.RollbackAsync();
|
||
|
||
// Пробрасываем ошибку дальше для обработки на уровне UI/Сервиса
|
||
throw new Exception($"Ошибка при создании заказа. Транзакция отменена. Детали: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
// Демонстрация отката (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()
|
||
{
|
||
var orders = new List<Order>();
|
||
await using var connection = _connectionFactory.CreateConnection();
|
||
await connection.OpenAsync();
|
||
|
||
string query = "SELECT Id, CustomerId, OrderDate, Status, TotalAmount FROM Orders";
|
||
await using var command = new SqlCommand(query, connection);
|
||
await using var reader = await command.ExecuteReaderAsync();
|
||
|
||
while (await reader.ReadAsync())
|
||
{
|
||
orders.Add(new Order
|
||
{
|
||
Id = reader.GetInt32(0),
|
||
CustomerId = reader.GetInt32(1),
|
||
OrderDate = reader.GetDateTime(2),
|
||
Status = reader.GetString(3),
|
||
TotalAmount = reader.GetDecimal(4)
|
||
});
|
||
}
|
||
return orders;
|
||
}
|
||
}
|