Add stored procedures demo (CommandType.StoredProcedure) and detailed teaching README

This commit is contained in:
Student 2026-10-05 06:12:40 +05:00
parent 2a0e22a08a
commit 9abe9d3db3
6 changed files with 1188 additions and 77 deletions

View File

@ -156,8 +156,77 @@ public class ProductRepository
// Метод Fill открывает подключение (если оно закрыто), выполняет запрос,
// заполняет DataTable и закрывает подключение (если оно было закрыто).
// Это и есть автономный режим.
// Fill() синхронный, поэтому оборачиваем его в Task.Run, чтобы не блокировать поток.
await Task.Run(() => adapter.Fill(table));
return table;
}
// ============ Вызов хранимой процедуры (CommandType.StoredProcedure) ============
// Демонстрация получения данных через хранимую процедуру dbo.GetProducts.
// В SQL передаётся не текст запроса, а ИМЯ процедуры.
public async Task<List<Product>> GetProductsByStoredProcedureAsync(int? categoryId)
{
var products = new List<Product>();
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
// Главное отличие от обычного запроса: CommandType.StoredProcedure + текст = имя процедуры
await using var command = new SqlCommand("dbo.GetProducts", connection)
{
CommandType = CommandType.StoredProcedure
};
// Параметры хранимой процедуры передаются точно так же, как параметры запроса.
// SQL Server сам подставит значения вместо @CategoryId внутри процедуры.
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = (object?)categoryId ?? DBNull.Value;
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
products.Add(new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Description = reader.IsDBNull(2) ? null : reader.GetString(2),
Price = reader.GetDecimal(3),
StockQuantity = reader.GetInt32(4),
CategoryId = reader.GetInt32(5)
});
}
return products;
}
// Вызов процедуры, которая изменяет данные, и получение числа затронутых строк.
// Здесь же обрабатывается ошибка, которую процедура отдаёт через RAISERROR.
public async Task<bool> UpdatePriceViaStoredProcedureAsync(int productId, decimal newPrice)
{
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync();
await using var command = new SqlCommand("dbo.UpdateProductPrice", connection)
{
CommandType = CommandType.StoredProcedure
};
command.Parameters.Add("@ProductId", SqlDbType.Int).Value = productId;
command.Parameters.Add("@NewPrice", SqlDbType.Decimal).Value = newPrice;
try
{
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
}
catch (SqlException ex)
{
// SqlException - это исключение самого SQL Server.
// В нём есть номер ошибки (Number) и текст сообщения от сервера.
Console.WriteLine($"Ошибка SQL Server (код {ex.Number}): {ex.Message}");
return false;
}
}
}

View File

@ -44,6 +44,7 @@ class Program
Console.WriteLine("8. Создать заказ (Демонстрация Transaction)");
Console.WriteLine("9. Просмотреть заказы");
Console.WriteLine("10. Демонстрация DataAdapter (Disconnected mode)");
Console.WriteLine("11. Демонстрация Stored Procedures");
Console.WriteLine("0. Выход");
Console.WriteLine("================================");
Console.Write("Выберите действие: ");
@ -84,6 +85,9 @@ class Program
case "10":
await DemoDataAdapter(shopService);
break;
case "11":
await DemoStoredProcedures(shopService);
break;
case "0":
exit = true;
break;
@ -294,4 +298,49 @@ class Program
Console.WriteLine($"{id}\t{name,-20}\t{price:C}");
}
}
// Демонстрация вызова хранимых процедур через ADO.NET
private static async Task DemoStoredProcedures(ShopService service)
{
Console.WriteLine("\n--- Демонстрация Stored Procedures ---");
await ShowAllCategories(service);
Console.Write("Введите Id категории (0 = все товары): ");
if (!int.TryParse(Console.ReadLine(), out int categoryId))
{
Console.WriteLine("Некорректный ввод.");
return;
}
// Вызов dbo.GetProducts. В C# мы передаём имя процедуры, а не SQL-запрос.
Console.WriteLine($"\nВызываем хранимую процедуру dbo.GetProducts @CategoryId = {(categoryId == 0 ? "NULL" : categoryId.ToString())}...");
var products = await service.GetProductsByStoredProcedureAsync(categoryId == 0 ? null : categoryId);
Console.WriteLine($"Процедура вернула товаров: {products.Count}");
foreach (var p in products)
{
Console.WriteLine($" [{p.Id}] {p.Name} — {p.Price:C} (остаток: {p.StockQuantity})");
}
if (products.Count == 0)
{
Console.WriteLine("\nНет данных для изменения цены.");
return;
}
Console.WriteLine($"\nВызываем dbo.UpdateProductPrice для товара [{products[0].Id}]: {products[0].Name}");
Console.Write("Новая цена (введите -5, чтобы увидеть ошибку от SQL Server): ");
if (!decimal.TryParse(Console.ReadLine(), out decimal newPrice))
{
Console.WriteLine("Некорректная цена.");
return;
}
// Процедура сама проверит цену и вернёт ошибку через RAISERROR.
// Мы её перехватываем как SqlException и показываем пользователю.
bool updated = await service.UpdatePriceViaStoredProcedureAsync(products[0].Id, newPrice);
Console.WriteLine(updated
? $"Цена товара [{products[0].Id}] успешно изменена на {newPrice:C}"
: "Цена не была изменена (смотрите сообщение об ошибке выше).");
}
}

View File

@ -42,6 +42,13 @@ public class ShopService
public Task<List<Category>> GetAllCategoriesAsync() => _categoryRepository.GetAllAsync();
public Task<List<Customer>> GetAllCustomersAsync() => _customerRepository.GetAllAsync();
// Прокси-методы для демонстрации хранимых процедур
public Task<List<Product>> GetProductsByStoredProcedureAsync(int? categoryId)
=> _productRepository.GetProductsByStoredProcedureAsync(categoryId);
public Task<bool> UpdatePriceViaStoredProcedureAsync(int productId, decimal newPrice)
=> _productRepository.UpdatePriceViaStoredProcedureAsync(productId, newPrice);
public async Task<int> CreateOrderAsync(int customerId, List<(int ProductId, int Quantity)> requestedItems)
{
// 1. Формируем заказ

1008
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,73 @@
-- 05_procedures.sql
-- Создание хранимых процедур
-- Хранимые процедуры (Stored Procedures).
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
-- умеет вызывать хранимую процедуру через CommandType.StoredProcedure.
USE [AdoNetShop];
GO
-- Процедура для получения товаров по категории
CREATE OR ALTER PROCEDURE GetProductsByCategory
@CategoryId INT
-- 1) Получить все товары (или только товары одной категории).
-- Если @CategoryId = NULL, возвращаются все товары.
IF OBJECT_ID('dbo.GetProducts', 'P') IS NOT NULL
DROP PROCEDURE dbo.GetProducts;
GO
CREATE PROCEDURE dbo.GetProducts
@CategoryId INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE CategoryId = @CategoryId;
WHERE @CategoryId IS NULL OR CategoryId = @CategoryId
ORDER BY Name;
END
GO
-- Процедура для безопасного обновления цены
CREATE OR ALTER PROCEDURE UpdateProductPrice
@ProductId INT,
@NewPrice DECIMAL(18,2)
-- 2) Получить один товар по идентификатору.
IF OBJECT_ID('dbo.GetProductById', 'P') IS NOT NULL
DROP PROCEDURE dbo.GetProductById;
GO
CREATE PROCEDURE dbo.GetProductById
@ProductId INT
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE Id = @ProductId;
END
GO
-- 3) Изменить цену товара с проверкой входных данных.
-- Простой пример того, как часть проверки можно вынести на сервер БД.
IF OBJECT_ID('dbo.UpdateProductPrice', 'P') IS NOT NULL
DROP PROCEDURE dbo.UpdateProductPrice;
GO
CREATE PROCEDURE dbo.UpdateProductPrice
@ProductId INT,
@NewPrice DECIMAL(18, 2)
AS
BEGIN
SET NOCOUNT ON;
IF @NewPrice <= 0
BEGIN
RAISERROR ('Цена должна быть больше нуля.', 16, 1);
-- RAISERROR: сами мы формируем сообщение об ошибке и отдаём его клиенту
RAISERROR ('Цена товара должна быть больше нуля.', 16, 1);
RETURN;
END
UPDATE Products
SET Price = @NewPrice
WHERE Id = @ProductId;
IF @@ROWCOUNT = 0
RAISERROR ('Товар с таким Id не найден.', 16, 1);
END
GO
GO

View File

@ -1,19 +1,74 @@
-- 06_test.sql
-- Тестовый скрипт (выполнять вручную для проверки базы)
-- Проверочные запросы. Выполняйте вручную после 01-05 скриптов.
USE [AdoNetShop];
GO
-- ============ Таблицы ============
SELECT * FROM Categories;
SELECT * FROM Products;
SELECT * FROM Customers;
SELECT * FROM Orders;
SELECT * FROM OrderItems;
-- Тест процедуры
EXEC GetProductsByCategory @CategoryId = 1;
-- ============ Примеры SELECT-запросов из приложения ============
-- Тест обновления цены (успешный)
EXEC UpdateProductPrice @ProductId = 1, @NewPrice = 1450.00;
SELECT * FROM Products WHERE Id = 1;
-- 1. Все товары (демонстрация SqlDataReader)
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products;
GO
-- Тест обновления цены (ошибка)
-- EXEC UpdateProductPrice @ProductId = 1, @NewPrice = -10.00;
-- 2. Один товар по Id (демонстрация параметризованного SqlCommand)
DECLARE @Id INT = 1;
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
FROM Products
WHERE Id = @Id;
GO
-- 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
ORDER BY p.Name;
GO
-- ============ Проверка хранимых процедур ============
-- Все товары
EXEC dbo.GetProducts;
GO
-- Товары только 1-й категории
EXEC dbo.GetProducts @CategoryId = 1;
GO
-- Один товар
EXEC dbo.GetProductById @ProductId = 1;
GO
-- Успешное изменение цены
EXEC dbo.UpdateProductPrice @ProductId = 1, @NewPrice = 1450.00;
SELECT Id, Name, Price FROM Products WHERE Id = 1;
GO
-- Ожидаемая ошибка: цена <= 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);
GO
-- Ожидаемая ошибка: 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);
GO