Add stored procedures demo (CommandType.StoredProcedure) and detailed teaching README
This commit is contained in:
parent
2a0e22a08a
commit
9abe9d3db3
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
: "Цена не была изменена (смотрите сообщение об ошибке выше).");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. Формируем заказ
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user