AdoNetShop/AdoNetShop/Data/OrderRepository.cs

232 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}