Compare commits
No commits in common. "4eef0f94324730f06f8b20f1aea04fd82c4e08d3" and "9abe9d3db36261db19a1215ffc0ea7fd2428e023" have entirely different histories.
4eef0f9432
...
9abe9d3db3
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
## Build results
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
## NuGet packages
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
packages/
|
||||
|
||||
## IDE
|
||||
.vs/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
## OS
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
3
AdoNetShop.slnx
Normal file
3
AdoNetShop.slnx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Solution>
|
||||
<Project Path="AdoNetShop/AdoNetShop.csproj" />
|
||||
</Solution>
|
||||
23
AdoNetShop/AdoNetShop.csproj
Normal file
23
AdoNetShop/AdoNetShop.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
38
AdoNetShop/Data/CategoryRepository.cs
Normal file
38
AdoNetShop/Data/CategoryRepository.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using System.Data;
|
||||
using AdoNetShop.Models;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace AdoNetShop.Data;
|
||||
|
||||
public class CategoryRepository
|
||||
{
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
|
||||
public CategoryRepository(DbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<List<Category>> GetAllAsync()
|
||||
{
|
||||
var categories = new List<Category>();
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
string query = "SELECT Id, Name FROM Categories";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
categories.Add(new Category
|
||||
{
|
||||
Id = reader.GetInt32(0),
|
||||
Name = reader.GetString(1)
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
40
AdoNetShop/Data/CustomerRepository.cs
Normal file
40
AdoNetShop/Data/CustomerRepository.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System.Data;
|
||||
using AdoNetShop.Models;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace AdoNetShop.Data;
|
||||
|
||||
public class CustomerRepository
|
||||
{
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
|
||||
public CustomerRepository(DbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<List<Customer>> GetAllAsync()
|
||||
{
|
||||
var customers = new List<Customer>();
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
string query = "SELECT Id, Name, Email, Phone FROM Customers";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
customers.Add(new Customer
|
||||
{
|
||||
Id = reader.GetInt32(0),
|
||||
Name = reader.GetString(1),
|
||||
Email = reader.GetString(2),
|
||||
Phone = reader.IsDBNull(3) ? null : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
return customers;
|
||||
}
|
||||
}
|
||||
22
AdoNetShop/Data/DbConnectionFactory.cs
Normal file
22
AdoNetShop/Data/DbConnectionFactory.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace AdoNetShop.Data;
|
||||
|
||||
public class DbConnectionFactory
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public DbConnectionFactory(IConfiguration configuration)
|
||||
{
|
||||
// Получаем строку подключения из конфигурации (appsettings.json)
|
||||
_connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||
}
|
||||
|
||||
// Фабричный метод для создания и настройки подключения
|
||||
public SqlConnection CreateConnection()
|
||||
{
|
||||
return new SqlConnection(_connectionString);
|
||||
}
|
||||
}
|
||||
117
AdoNetShop/Data/OrderRepository.cs
Normal file
117
AdoNetShop/Data/OrderRepository.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Получение списка заказов (простая реализация)
|
||||
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;
|
||||
}
|
||||
}
|
||||
232
AdoNetShop/Data/ProductRepository.cs
Normal file
232
AdoNetShop/Data/ProductRepository.cs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
using System.Data;
|
||||
using AdoNetShop.Models;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace AdoNetShop.Data;
|
||||
|
||||
public class ProductRepository
|
||||
{
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
|
||||
public ProductRepository(DbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
// CRUD: Read All (Получение списка)
|
||||
public async Task<List<Product>> GetAllAsync()
|
||||
{
|
||||
var products = new List<Product>();
|
||||
|
||||
// Используем await using для автоматического освобождения ресурсов
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(); // Открываем подключение
|
||||
|
||||
string query = "SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products";
|
||||
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
|
||||
// SqlDataReader обеспечивает потоковое чтение результатов (forward-only)
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
products.Add(new Product
|
||||
{
|
||||
Id = reader.GetInt32(reader.GetOrdinal("Id")),
|
||||
Name = reader.GetString(reader.GetOrdinal("Name")),
|
||||
// Обработка возможного NULL значения для Description
|
||||
Description = reader.IsDBNull(reader.GetOrdinal("Description")) ? null : reader.GetString(reader.GetOrdinal("Description")),
|
||||
Price = reader.GetDecimal(reader.GetOrdinal("Price")),
|
||||
StockQuantity = reader.GetInt32(reader.GetOrdinal("StockQuantity")),
|
||||
CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId"))
|
||||
});
|
||||
}
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
// CRUD: Read One (Получение одного товара по ID)
|
||||
public async Task<Product?> GetByIdAsync(int id)
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Использование параметров для предотвращения SQL Injections
|
||||
string query = "SELECT Id, Name, Description, Price, StockQuantity, CategoryId FROM Products WHERE Id = @Id";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
if (await reader.ReadAsync())
|
||||
{
|
||||
return new Product
|
||||
{
|
||||
Id = reader.GetInt32(reader.GetOrdinal("Id")),
|
||||
Name = reader.GetString(reader.GetOrdinal("Name")),
|
||||
Description = reader.IsDBNull(reader.GetOrdinal("Description")) ? null : reader.GetString(reader.GetOrdinal("Description")),
|
||||
Price = reader.GetDecimal(reader.GetOrdinal("Price")),
|
||||
StockQuantity = reader.GetInt32(reader.GetOrdinal("StockQuantity")),
|
||||
CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId"))
|
||||
};
|
||||
}
|
||||
|
||||
return null; // Если товар не найден
|
||||
}
|
||||
|
||||
// CRUD: Create (Добавление)
|
||||
public async Task<int> CreateAsync(Product product)
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Используем OUTPUT INSERTED.Id или SCOPE_IDENTITY() для получения сгенерированного Id
|
||||
string query = @"
|
||||
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId)
|
||||
VALUES (@Name, @Description, @Price, @StockQuantity, @CategoryId);
|
||||
SELECT SCOPE_IDENTITY();";
|
||||
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
|
||||
// Явное указание типов параметров (хорошая практика)
|
||||
command.Parameters.Add("@Name", SqlDbType.NVarChar, 200).Value = product.Name;
|
||||
command.Parameters.Add("@Description", SqlDbType.NVarChar, -1).Value = (object?)product.Description ?? DBNull.Value;
|
||||
command.Parameters.Add("@Price", SqlDbType.Decimal).Value = product.Price;
|
||||
command.Parameters.Add("@StockQuantity", SqlDbType.Int).Value = product.StockQuantity;
|
||||
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = product.CategoryId;
|
||||
|
||||
// ExecuteScalarAsync используется для получения единственного значения (в данном случае Id)
|
||||
var newId = await command.ExecuteScalarAsync();
|
||||
return Convert.ToInt32(newId);
|
||||
}
|
||||
|
||||
// CRUD: Update (Обновление)
|
||||
public async Task<bool> UpdateAsync(Product product)
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
string query = @"
|
||||
UPDATE Products
|
||||
SET Name = @Name, Description = @Description, Price = @Price,
|
||||
StockQuantity = @StockQuantity, CategoryId = @CategoryId
|
||||
WHERE Id = @Id";
|
||||
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
|
||||
command.Parameters.Add("@Id", SqlDbType.Int).Value = product.Id;
|
||||
command.Parameters.Add("@Name", SqlDbType.NVarChar, 200).Value = product.Name;
|
||||
command.Parameters.Add("@Description", SqlDbType.NVarChar, -1).Value = (object?)product.Description ?? DBNull.Value;
|
||||
command.Parameters.Add("@Price", SqlDbType.Decimal).Value = product.Price;
|
||||
command.Parameters.Add("@StockQuantity", SqlDbType.Int).Value = product.StockQuantity;
|
||||
command.Parameters.Add("@CategoryId", SqlDbType.Int).Value = product.CategoryId;
|
||||
|
||||
// ExecuteNonQueryAsync используется для запросов INSERT/UPDATE/DELETE
|
||||
int rowsAffected = await command.ExecuteNonQueryAsync();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
// CRUD: Delete (Удаление)
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
string query = "DELETE FROM Products WHERE Id = @Id";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
|
||||
|
||||
int rowsAffected = await command.ExecuteNonQueryAsync();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
// Демонстрация DataAdapter и автономной модели (Disconnected Mode)
|
||||
public async Task<DataTable> GetProductsTableAsync()
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
|
||||
string query = "SELECT Id, Name, Price, StockQuantity FROM Products";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
|
||||
// SqlDataAdapter служит мостом между БД и DataSet/DataTable
|
||||
using var adapter = new SqlDataAdapter(command);
|
||||
var table = new DataTable("Products");
|
||||
|
||||
// Метод 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
AdoNetShop/Models/Category.cs
Normal file
7
AdoNetShop/Models/Category.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace AdoNetShop.Models;
|
||||
|
||||
public class Category
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
9
AdoNetShop/Models/Customer.cs
Normal file
9
AdoNetShop/Models/Customer.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace AdoNetShop.Models;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
12
AdoNetShop/Models/Order.cs
Normal file
12
AdoNetShop/Models/Order.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace AdoNetShop.Models;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CustomerId { get; set; }
|
||||
public DateTime OrderDate { get; set; }
|
||||
public string Status { get; set; } = "New";
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
|
||||
}
|
||||
10
AdoNetShop/Models/OrderItem.cs
Normal file
10
AdoNetShop/Models/OrderItem.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace AdoNetShop.Models;
|
||||
|
||||
public class OrderItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int OrderId { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
14
AdoNetShop/Models/Product.cs
Normal file
14
AdoNetShop/Models/Product.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace AdoNetShop.Models;
|
||||
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public int StockQuantity { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
// Навигационное свойство (заполняется опционально)
|
||||
public Category? Category { get; set; }
|
||||
}
|
||||
346
AdoNetShop/Program.cs
Normal file
346
AdoNetShop/Program.cs
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
using System.Data;
|
||||
using AdoNetShop.Data;
|
||||
using AdoNetShop.Models;
|
||||
using AdoNetShop.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace AdoNetShop;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
// 1. Настройка конфигурации для чтения appsettings.json
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
|
||||
// 2. "Ручной" Dependency Injection
|
||||
var connectionFactory = new DbConnectionFactory(configuration);
|
||||
|
||||
var productRepo = new ProductRepository(connectionFactory);
|
||||
var categoryRepo = new CategoryRepository(connectionFactory);
|
||||
var customerRepo = new CustomerRepository(connectionFactory);
|
||||
var orderRepo = new OrderRepository(connectionFactory);
|
||||
|
||||
var shopService = new ShopService(productRepo, categoryRepo, customerRepo, orderRepo);
|
||||
|
||||
// 3. Главный цикл приложения
|
||||
bool exit = false;
|
||||
while (!exit)
|
||||
{
|
||||
Console.Clear();
|
||||
Console.WriteLine("================================");
|
||||
Console.WriteLine(" ADO.NET SHOP");
|
||||
Console.WriteLine("================================");
|
||||
Console.WriteLine("1. Список товаров");
|
||||
Console.WriteLine("2. Найти товар по ID");
|
||||
Console.WriteLine("3. Добавить товар");
|
||||
Console.WriteLine("4. Изменить товар");
|
||||
Console.WriteLine("5. Удалить товар");
|
||||
Console.WriteLine("6. Список категорий");
|
||||
Console.WriteLine("7. Список клиентов");
|
||||
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("Выберите действие: ");
|
||||
|
||||
var input = Console.ReadLine();
|
||||
|
||||
try
|
||||
{
|
||||
switch (input)
|
||||
{
|
||||
case "1":
|
||||
await ShowAllProducts(shopService);
|
||||
break;
|
||||
case "2":
|
||||
await FindProduct(shopService);
|
||||
break;
|
||||
case "3":
|
||||
await AddProduct(shopService);
|
||||
break;
|
||||
case "4":
|
||||
await UpdateProduct(shopService);
|
||||
break;
|
||||
case "5":
|
||||
await DeleteProduct(shopService);
|
||||
break;
|
||||
case "6":
|
||||
await ShowAllCategories(shopService);
|
||||
break;
|
||||
case "7":
|
||||
await ShowAllCustomers(shopService);
|
||||
break;
|
||||
case "8":
|
||||
await CreateOrder(shopService);
|
||||
break;
|
||||
case "9":
|
||||
await ShowAllOrders(shopService);
|
||||
break;
|
||||
case "10":
|
||||
await DemoDataAdapter(shopService);
|
||||
break;
|
||||
case "11":
|
||||
await DemoStoredProcedures(shopService);
|
||||
break;
|
||||
case "0":
|
||||
exit = true;
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("Неверный ввод.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[ОШИБКА]: {ex.Message}");
|
||||
if(ex.InnerException != null)
|
||||
{
|
||||
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
|
||||
}
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
if (!exit)
|
||||
{
|
||||
Console.WriteLine("\nНажмите любую клавишу для продолжения...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ShowAllProducts(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Список товаров ---");
|
||||
var products = await service.GetAllProductsAsync();
|
||||
foreach (var p in products)
|
||||
{
|
||||
Console.WriteLine($"[{p.Id}] {p.Name} | Цена: {p.Price:C} | Остаток: {p.StockQuantity} | Кат: {p.CategoryId}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task FindProduct(ShopService service)
|
||||
{
|
||||
Console.Write("\nВведите ID товара: ");
|
||||
if (int.TryParse(Console.ReadLine(), out int id))
|
||||
{
|
||||
var product = await service.GetProductByIdAsync(id);
|
||||
if (product != null)
|
||||
{
|
||||
Console.WriteLine($"Найдено: [{product.Id}] {product.Name} - {product.Description} | {product.Price:C} | {product.StockQuantity} шт.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Товар не найден.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Некорректный ID.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AddProduct(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Добавление товара ---");
|
||||
|
||||
Console.Write("Название: ");
|
||||
string name = Console.ReadLine() ?? "";
|
||||
|
||||
Console.Write("Описание: ");
|
||||
string desc = Console.ReadLine() ?? "";
|
||||
|
||||
Console.Write("Цена: ");
|
||||
if (!decimal.TryParse(Console.ReadLine(), out decimal price)) throw new ArgumentException("Неверный формат цены.");
|
||||
|
||||
Console.Write("Количество: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int quantity)) throw new ArgumentException("Неверный формат количества.");
|
||||
|
||||
Console.Write("ID Категории: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int catId)) throw new ArgumentException("Неверный формат ID категории.");
|
||||
|
||||
var product = new Product
|
||||
{
|
||||
Name = name,
|
||||
Description = desc,
|
||||
Price = price,
|
||||
StockQuantity = quantity,
|
||||
CategoryId = catId
|
||||
};
|
||||
|
||||
int newId = await service.CreateProductAsync(product);
|
||||
Console.WriteLine($"Товар успешно добавлен. Новый ID: {newId}");
|
||||
}
|
||||
|
||||
private static async Task UpdateProduct(ShopService service)
|
||||
{
|
||||
Console.Write("\nВведите ID товара для обновления: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int id)) return;
|
||||
|
||||
var product = await service.GetProductByIdAsync(id);
|
||||
if (product == null)
|
||||
{
|
||||
Console.WriteLine("Товар не найден.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Текущее название: {product.Name}");
|
||||
Console.Write("Новое название (оставьте пустым, чтобы не менять): ");
|
||||
string name = Console.ReadLine() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(name)) product.Name = name;
|
||||
|
||||
Console.WriteLine($"Текущая цена: {product.Price}");
|
||||
Console.Write("Новая цена (оставьте пустым, чтобы не менять): ");
|
||||
string priceStr = Console.ReadLine() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(priceStr) && decimal.TryParse(priceStr, out decimal price)) product.Price = price;
|
||||
|
||||
bool updated = await service.UpdateProductAsync(product);
|
||||
Console.WriteLine(updated ? "Товар обновлен." : "Не удалось обновить товар.");
|
||||
}
|
||||
|
||||
private static async Task DeleteProduct(ShopService service)
|
||||
{
|
||||
Console.Write("\nВведите ID товара для удаления: ");
|
||||
if (int.TryParse(Console.ReadLine(), out int id))
|
||||
{
|
||||
bool deleted = await service.DeleteProductAsync(id);
|
||||
Console.WriteLine(deleted ? "Товар удален." : "Товар не найден или не может быть удален.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ShowAllCategories(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Категории ---");
|
||||
var cats = await service.GetAllCategoriesAsync();
|
||||
foreach (var c in cats) Console.WriteLine($"[{c.Id}] {c.Name}");
|
||||
}
|
||||
|
||||
private static async Task ShowAllCustomers(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Клиенты ---");
|
||||
var customers = await service.GetAllCustomersAsync();
|
||||
foreach (var c in customers) Console.WriteLine($"[{c.Id}] {c.Name} ({c.Email})");
|
||||
}
|
||||
|
||||
private static async Task CreateOrder(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Создание заказа ---");
|
||||
|
||||
await ShowAllCustomers(service);
|
||||
Console.Write("Введите ID клиента: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int customerId)) return;
|
||||
|
||||
var items = new List<(int ProductId, int Quantity)>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("Введите ID товара (или 0 для завершения): ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int productId)) continue;
|
||||
if (productId == 0) break;
|
||||
|
||||
Console.Write("Введите количество: ");
|
||||
if (!int.TryParse(Console.ReadLine(), out int quantity) || quantity <= 0)
|
||||
{
|
||||
Console.WriteLine("Неверное количество.");
|
||||
continue;
|
||||
}
|
||||
|
||||
items.Add((productId, quantity));
|
||||
}
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Заказ отменен (нет товаров).");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Попытка создать заказ в БД (Транзакция)...");
|
||||
int orderId = await service.CreateOrderAsync(customerId, items);
|
||||
Console.WriteLine($"Заказ успешно создан! ID: {orderId}");
|
||||
}
|
||||
|
||||
private static async Task ShowAllOrders(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Заказы ---");
|
||||
var orders = await service.GetAllOrdersAsync();
|
||||
foreach (var o in orders)
|
||||
{
|
||||
Console.WriteLine($"[{o.Id}] Клиент: {o.CustomerId} | Сумма: {o.TotalAmount:C} | Дата: {o.OrderDate:g} | Статус: {o.Status}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DemoDataAdapter(ShopService service)
|
||||
{
|
||||
Console.WriteLine("\n--- Демонстрация DataAdapter (Disconnected mode) ---");
|
||||
Console.WriteLine("Запрашиваем данные через DataAdapter.Fill()...");
|
||||
|
||||
DataTable table = await service.GetProductsTableAsync();
|
||||
|
||||
Console.WriteLine($"Получено строк: {table.Rows.Count}");
|
||||
Console.WriteLine("Подключение к БД уже закрыто. Данные находятся в памяти (DataTable).\n");
|
||||
|
||||
Console.WriteLine("Id\tName\t\t\tPrice");
|
||||
Console.WriteLine("--------------------------------------------------");
|
||||
|
||||
// Чтение данных из автономного DataTable
|
||||
foreach (DataRow row in table.Rows)
|
||||
{
|
||||
int id = (int)row["Id"];
|
||||
string name = (string)row["Name"];
|
||||
decimal price = (decimal)row["Price"];
|
||||
|
||||
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}"
|
||||
: "Цена не была изменена (смотрите сообщение об ошибке выше).");
|
||||
}
|
||||
}
|
||||
99
AdoNetShop/Services/ShopService.cs
Normal file
99
AdoNetShop/Services/ShopService.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
using AdoNetShop.Data;
|
||||
using AdoNetShop.Models;
|
||||
|
||||
namespace AdoNetShop.Services;
|
||||
|
||||
// Сервисы могут содержать дополнительную бизнес-логику,
|
||||
// но в рамках этой лабы они выступают простыми прокси к репозиториям.
|
||||
public class ShopService
|
||||
{
|
||||
private readonly ProductRepository _productRepository;
|
||||
private readonly CategoryRepository _categoryRepository;
|
||||
private readonly CustomerRepository _customerRepository;
|
||||
private readonly OrderRepository _orderRepository;
|
||||
|
||||
public ShopService(
|
||||
ProductRepository productRepository,
|
||||
CategoryRepository categoryRepository,
|
||||
CustomerRepository customerRepository,
|
||||
OrderRepository orderRepository)
|
||||
{
|
||||
_productRepository = productRepository;
|
||||
_categoryRepository = categoryRepository;
|
||||
_customerRepository = customerRepository;
|
||||
_orderRepository = orderRepository;
|
||||
}
|
||||
|
||||
public Task<List<Product>> GetAllProductsAsync() => _productRepository.GetAllAsync();
|
||||
public Task<Product?> GetProductByIdAsync(int id) => _productRepository.GetByIdAsync(id);
|
||||
|
||||
public async Task<int> CreateProductAsync(Product product)
|
||||
{
|
||||
// Пример простой валидации на уровне сервиса
|
||||
if (product.Price <= 0) throw new ArgumentException("Price must be greater than 0");
|
||||
if (product.StockQuantity < 0) throw new ArgumentException("Stock cannot be negative");
|
||||
|
||||
return await _productRepository.CreateAsync(product);
|
||||
}
|
||||
|
||||
public Task<bool> UpdateProductAsync(Product product) => _productRepository.UpdateAsync(product);
|
||||
public Task<bool> DeleteProductAsync(int id) => _productRepository.DeleteAsync(id);
|
||||
|
||||
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. Формируем заказ
|
||||
var order = new Order
|
||||
{
|
||||
CustomerId = customerId,
|
||||
OrderDate = DateTime.Now,
|
||||
Status = "Created",
|
||||
Items = new List<OrderItem>()
|
||||
};
|
||||
|
||||
decimal totalAmount = 0;
|
||||
|
||||
// 2. Получаем актуальные цены товаров
|
||||
foreach (var reqItem in requestedItems)
|
||||
{
|
||||
var product = await _productRepository.GetByIdAsync(reqItem.ProductId);
|
||||
if (product == null)
|
||||
{
|
||||
throw new ArgumentException($"Product with ID {reqItem.ProductId} not found.");
|
||||
}
|
||||
|
||||
if(product.StockQuantity < reqItem.Quantity)
|
||||
{
|
||||
throw new ArgumentException($"Not enough stock for product '{product.Name}'. Available: {product.StockQuantity}, Requested: {reqItem.Quantity}");
|
||||
}
|
||||
|
||||
var orderItem = new OrderItem
|
||||
{
|
||||
ProductId = reqItem.ProductId,
|
||||
Quantity = reqItem.Quantity,
|
||||
Price = product.Price
|
||||
};
|
||||
|
||||
order.Items.Add(orderItem);
|
||||
totalAmount += (reqItem.Quantity * product.Price);
|
||||
}
|
||||
|
||||
order.TotalAmount = totalAmount;
|
||||
|
||||
// 3. Вызываем репозиторий для сохранения (внутри будет транзакция)
|
||||
return await _orderRepository.CreateOrderWithTransactionAsync(order);
|
||||
}
|
||||
|
||||
public Task<List<Order>> GetAllOrdersAsync() => _orderRepository.GetAllOrdersBasicAsync();
|
||||
|
||||
public Task<System.Data.DataTable> GetProductsTableAsync() => _productRepository.GetProductsTableAsync();
|
||||
}
|
||||
5
AdoNetShop/appsettings.json
Normal file
5
AdoNetShop/appsettings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
971
README.md
971
README.md
|
|
@ -1,3 +1,970 @@
|
|||
# AdoNetShop
|
||||
# AdoNetShop — лабораторная работа по технологии ADO.NET
|
||||
|
||||
??????? ???????????? ?????? ?? ?????????? ADO.NET (C# + Microsoft SQL Server, ??? ORM)
|
||||
**Что это за проект.** Это небольшой учебный интернет-магазин, написанный на C# (.NET 8).
|
||||
Всё общение с базой данных Microsoft SQL Server выполняется **вручную через ADO.NET**
|
||||
(пакет `Microsoft.Data.SqlClient`). В проекте **нет** Entity Framework, Dapper, NHibernate
|
||||
и любых других ORM — нам важно увидеть, как приложение общается с БД «на низком уровне».
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Что такое ADO.NET и зачем он нужен](#1-что-такое-adonet-и-зачем-он-нужен)
|
||||
2. [Что такое Provider (провайдер данных)](#2-что-такое-provider-провайдер-данных)
|
||||
3. [Основные классы ADO.NET, которые используются в проекте](#3-основные-классы-adonet-используемые-в-проекте)
|
||||
- 3.1 `SqlConnection` — подключение к базе
|
||||
- 3.2 `SqlCommand` — команда (запрос)
|
||||
- 3.3 `SqlParameter` — параметры и защита от SQL Injection
|
||||
- 3.4 `SqlDataReader` — чтение результатов (connected)
|
||||
- 3.5 `SqlDataAdapter` и `DataTable` — disconnected
|
||||
- 3.6 `SqlTransaction` — транзакции
|
||||
4. [Connected vs Disconnected: в чём разница](#4-connected-vs-disconnected-в-чём-разница)
|
||||
5. [Структура проекта](#5-структура-проекта)
|
||||
6. [Структура базы данных](#6-структура-базы-данных)
|
||||
7. [Как установить SQL Server](#7-как-установить-sql-server)
|
||||
8. [Как создать базу данных и выполнить SQL-скрипты](#8-как-создать-базу-данных-и-выполнить-sql-скрипты)
|
||||
9. [Как изменить строку подключения (Connection String)](#9-как-изменить-строку-подключения-connection-string)
|
||||
10. [Как запустить проект](#10-как-запустить-проект)
|
||||
11. [Что умеет программа (демонстрация преподавателю)](#11-что-умеет-программа-демонстрация-преподавателю)
|
||||
12. [Обработка ошибок и валидация](#12-обработка-ошибок-и-валидация)
|
||||
13. [Какие файлы нужно изучить в первую очередь](#13-какие-файлы-нужно-изучить-в-первую-очередь)
|
||||
14. [Частые проблемы и их решения](#14-частые-проблемы-и-их-решения)
|
||||
|
||||
---
|
||||
|
||||
## 1. Что такое ADO.NET и зачем он нужен
|
||||
|
||||
**ADO.NET** (ActiveX Data Objects .NET) — это набор классов, входящих в состав .NET,
|
||||
который позволяет приложению на C# общаться с источниками данных: реляционными базами
|
||||
данных, XML-файлами и т.д.
|
||||
|
||||
Проще всего представить ADO.NET так:
|
||||
|
||||
```
|
||||
┌──────────────────────┐ ADO.NET ┌──────────────────────┐
|
||||
│ Приложение на C# │ ◄──────────────────► │ SQL Server │
|
||||
│ (ваш код) │ команды и результаты │ (данные) │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
Приложение не «знает» SQL Server напрямую. Между ними стоит слой ADO.NET, который
|
||||
переводит вызовы C# (открыть соединение, выполнить запрос, прочитать результат)
|
||||
в сетевые сообщения протокола SQL Server.
|
||||
|
||||
**Зачем это нужно:**
|
||||
|
||||
1. **Прямой контроль над SQL.** Вы сами пишете запрос — значит, вы можете использовать
|
||||
всё, что умеет конкретная СУБД: индексы, хинты, `JOIN`, оконные функции, `OUTPUT`,
|
||||
временные таблицы. Никакой ORM не даст вам такой свободы без «хаков».
|
||||
2. **Производительность.** ORM делает много «умной» работы (отслеживание изменений,
|
||||
ленивая загрузка, кэширование), которая часто не нужна. Прямой ADO.NET — это
|
||||
минимальные накладные расходы. Для учебных и для production-задач вродя отчётов
|
||||
ADO.NET часто быстрее.
|
||||
3. **Понимание того, как всё работает на самом деле.** Даже если потом вы будете писать
|
||||
на Entity Framework, понимание ADO.NET помогает понять, что ORM делает под капотом.
|
||||
4. **Не зависимость от лишних зависимостей.** ADO.NET — часть платформы .NET,
|
||||
нужен только один NuGet-пакет провайдера.
|
||||
|
||||
---
|
||||
|
||||
## 2. Что такое Provider (провайдер данных)
|
||||
|
||||
ADO.NET — это набор **интерфейсов и абстракций**, но он не умеет сам ходить в базу.
|
||||
Для каждой СУБД существует конкретная реализация — **провайдер данных** (Data Provider).
|
||||
|
||||
| СУБД | Провайдер (пакет NuGet) |
|
||||
|---|---|
|
||||
| Microsoft SQL Server | `Microsoft.Data.SqlClient` |
|
||||
| PostgreSQL | `Npgsql` |
|
||||
| MySQL / MariaDB | `MySqlConnector` |
|
||||
| Oracle | `Oracle.ManagedDataAccess` |
|
||||
| SQLite | `Microsoft.Data.Sqlite` |
|
||||
|
||||
В нашем проекте используется **`Microsoft.Data.SqlClient`** — официальный провайдер
|
||||
для SQL Server. Именно он предоставляет классы `SqlConnection`, `SqlCommand`,
|
||||
`SqlDataReader`, `SqlDataAdapter`, `SqlTransaction`.
|
||||
|
||||
> **Зачем провайдер вынесен в отдельный пакет, а не встроен в .NET?**
|
||||
> Разработчики .NET не могут включить в платформу драйверы ко всем существующим СУБД —
|
||||
> это слишком много кода и лицензионные ограничения. Поэтому каждый провайдер —
|
||||
> отдельный NuGet-пакет, который подключается одной строкой в `.csproj`:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Основные классы ADO.NET, используемые в проекте
|
||||
|
||||
### 3.1 `SqlConnection` — подключение к базе
|
||||
|
||||
`SqlConnection` — это объект, представляющий **физическое соединение** с SQL Server.
|
||||
|
||||
Создаётся из **строки подключения** (connection string), которая содержит всю
|
||||
необходимую информацию: адрес сервера, имя базы, способ аутентификации.
|
||||
|
||||
```csharp
|
||||
// Файл: AdoNetShop/Data/DbConnectionFactory.cs
|
||||
await using var connection = new SqlConnection(connectionString);
|
||||
|
||||
// Открываем физическое подключение к SQL Server
|
||||
await connection.OpenAsync();
|
||||
```
|
||||
|
||||
Три важных правила:
|
||||
|
||||
1. **Создать** — `new SqlConnection(connectionString)`. Сам по себе объект ещё не
|
||||
подключён, это просто «заготовка».
|
||||
2. **Открыть** — `Open()` / `OpenAsync()`. Только после этого можно выполнять команды.
|
||||
Строка подключения читается именно в этот момент (ленивое подключение).
|
||||
3. **Закрыть/освободить** — `Close()` / `Dispose()`. Подключения — ограниченный ресурс
|
||||
(пул ограничен по числу соединений), поэтому их обязательно нужно закрывать.
|
||||
|
||||
В проекте мы **нигде не вызываем `Close()` вручную**, а всегда используем `await using`:
|
||||
|
||||
```csharp
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
// ... работа с базой ...
|
||||
// при выходе из блока соединение автоматически закроется и освободится,
|
||||
// даже если внутри произошло исключение
|
||||
```
|
||||
|
||||
`using` / `await using` — это конструкция языка C#, которая гарантирует вызов
|
||||
`Dispose()`. Для `SqlConnection`, `SqlCommand`, `SqlDataReader` это обязательно,
|
||||
потому что они используют неуправляемые ресурсы.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `SqlCommand` — команда (запрос)
|
||||
|
||||
`SqlCommand` — это объект, который содержит **что именно** мы хотим сделать с базой:
|
||||
SQL-запрос, имя хранимой процедуры или текст команды.
|
||||
|
||||
```csharp
|
||||
await using var command = new SqlCommand(
|
||||
"SELECT Id, Name, Price FROM Products WHERE CategoryId = @CategoryId",
|
||||
connection);
|
||||
```
|
||||
|
||||
Три способа выполнить команду:
|
||||
|
||||
| Метод | Что возвращает | Когда использовать |
|
||||
|---|---|---|
|
||||
| `ExecuteReaderAsync()` | `SqlDataReader` | `SELECT` — когда нужно прочитать много строк |
|
||||
| `ExecuteScalarAsync()` | одно значение (`object`) | `SELECT COUNT(*)`, `SELECT SCOPE_IDENTITY()` |
|
||||
| `ExecuteNonQueryAsync()` | число затронутых строк (`int`) | `INSERT`, `UPDATE`, `DELETE` |
|
||||
|
||||
Примеры из проекта (`AdoNetShop/Data/ProductRepository.cs`):
|
||||
|
||||
```csharp
|
||||
// ExecuteScalarAsync — получить Id только что вставленного товара
|
||||
// (SCOPE_IDENTITY() возвращает Id, который сгенерировала база для новой строки)
|
||||
string query = @"
|
||||
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId)
|
||||
VALUES (@Name, @Description, @Price, @StockQuantity, @CategoryId);
|
||||
SELECT SCOPE_IDENTITY();";
|
||||
var newId = await command.ExecuteScalarAsync();
|
||||
return Convert.ToInt32(newId);
|
||||
|
||||
// ExecuteNonQueryAsync — обновить товар, вернётся количество затронутых строк
|
||||
int rowsAffected = await command.ExecuteNonQueryAsync();
|
||||
return rowsAffected > 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `SqlParameter` — параметры и защита от SQL Injection
|
||||
|
||||
**Это самая важная тема для защиты лабораторной.**
|
||||
|
||||
Правило №1 при работе с любой БД: **никогда не собирайте SQL-запрос склейкой строк.**
|
||||
|
||||
❌ **НЕЛЬЗЯ** так — это SQL Injection:
|
||||
|
||||
```csharp
|
||||
// ОПАСНО! Не делайте так никогда.
|
||||
string query = "SELECT * FROM Products WHERE Name = '" + name + "'";
|
||||
```
|
||||
|
||||
Почему это опасно: если пользователь введёт в поле имя товара
|
||||
|
||||
```
|
||||
' OR '1'='1
|
||||
```
|
||||
|
||||
то запрос превратится в `SELECT * FROM Products WHERE Name = '' OR '1'='1'` —
|
||||
вернётся вся таблица. Это и есть **SQL Injection**: злоумышленник может подставить
|
||||
`'; DROP TABLE Products; --` и удалить таблицу.
|
||||
|
||||
✅ **ПРАВИЛЬНО** так — использовать параметры:
|
||||
|
||||
```csharp
|
||||
// Безопасно. Значение не «вклеивается» в текст запроса,
|
||||
// база получает его как отдельный типизированный параметр.
|
||||
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
|
||||
```
|
||||
|
||||
Что здесь важно:
|
||||
|
||||
* `@Id` — имя параметра в SQL-тексте, оно **не является** значением.
|
||||
* `SqlDbType.Int` — явно указываем SQL-тип. Это правильнее, чем
|
||||
`AddWithValue`, потому что `AddWithValue` передаёт .NET-тип, а SQL Server может
|
||||
неправильно его интерпретировать (например, объект станет `nvarchar`, и запрос
|
||||
по числовому полю будет работать медленнее или вообще иначе).
|
||||
* `SqlClient` отправляет значение и тип **отдельно от текста запроса**, поэтому
|
||||
вставить произвольный SQL через значение параметра невозможно.
|
||||
|
||||
Также обратите внимание, что текст SQL в проекте всегда пишется константой
|
||||
(через `@"..."` — многострочная строка), а пользовательские данные подставляются
|
||||
**только** через `Parameters`. Поиск по коду не должен находить конкатенацию
|
||||
пользовательских данных с SQL.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `SqlDataReader` — чтение результатов запроса
|
||||
|
||||
`SqlDataReader` — «читатель» результата `SELECT`. Он работает в режиме
|
||||
**forward-only** (только вперёд) и **read-only** (только чтение).
|
||||
|
||||
Что это значит на практике:
|
||||
|
||||
* Данные приходят **по одной строке**, а не сразу все. Это экономит память —
|
||||
можно обработать миллион строк, не загружая их в память целиком.
|
||||
* Нельзя вернуться на предыдущую строку (нельзя «отмотать назад»).
|
||||
* Пока `reader` открыт, **нельзя** выполнять другие команды на том же подключении.
|
||||
|
||||
Пример из проекта:
|
||||
|
||||
```csharp
|
||||
// Выполняем SELECT и получаем потоковое чтение результата
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
// пока reader.ReadAsync() возвращает true — читаем очередную строку
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
int id = reader.GetInt32(reader.GetOrdinal("Id"));
|
||||
string name = reader.GetString(reader.GetOrdinal("Name"));
|
||||
|
||||
// Если в поле может быть NULL, проверяем это отдельно
|
||||
string? description = reader.IsDBNull(reader.GetOrdinal("Description"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("Description"));
|
||||
|
||||
products.Add(new Product { Id = id, Name = name, Description = description });
|
||||
}
|
||||
```
|
||||
|
||||
Методы `GetInt32`, `GetString`, `GetDecimal` читают значение нужного типа.
|
||||
`GetOrdinal("Name")` возвращает номер колонки по её названию — читать по номеру
|
||||
(`reader.GetString(0)`) тоже можно, но по названию понятнее и безопаснее
|
||||
(если порядок колонок в запросе изменится, код продолжит работать).
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `SqlDataAdapter` и `DataTable` — disconnected (автономный) режим
|
||||
|
||||
Мы уже посмотрели на **connected** подход: подключение открыто, `SqlDataReader`
|
||||
читает строки по одной. Он быстрый, но подключение занято всё время чтения.
|
||||
|
||||
**Disconnected (автономный) подход** работает иначе:
|
||||
|
||||
1. `SqlDataAdapter` получает команду с запросом.
|
||||
2. Сам открывает подключение (если оно закрыто), выполняет `SELECT`
|
||||
и **забирает весь результат в память**.
|
||||
3. Заполняет `DataTable` (или `DataSet`) — таблицу, которая живёт в памяти приложения.
|
||||
4. **Сам закрывает подключение** (если открывал его).
|
||||
|
||||
После этого вы работаете с `DataTable` как с обычной таблицей в памяти,
|
||||
а база данных **полностью свободна** и не ждёт вас.
|
||||
|
||||
Пример из проекта (`AdoNetShop/Data/ProductRepository.cs`):
|
||||
|
||||
```csharp
|
||||
public async Task<DataTable> GetProductsTableAsync()
|
||||
{
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
|
||||
string query = "SELECT Id, Name, Price, StockQuantity FROM Products";
|
||||
await using var command = new SqlCommand(query, connection);
|
||||
|
||||
// SqlDataAdapter — «мост» между источником данных и DataTable
|
||||
using var adapter = new SqlDataAdapter(command);
|
||||
var table = new DataTable("Products");
|
||||
|
||||
// Fill() сам откроет подключение, выполнит запрос,
|
||||
// заполнит DataTable и закроет подключение.
|
||||
// Fill() синхронный, поэтому оборачиваем его в Task.Run.
|
||||
await Task.Run(() => adapter.Fill(table));
|
||||
|
||||
return table;
|
||||
}
|
||||
```
|
||||
|
||||
А дальше в `Program.cs` мы работаем с таблицей **без базы данных**:
|
||||
|
||||
```csharp
|
||||
DataTable table = await service.GetProductsTableAsync();
|
||||
|
||||
foreach (DataRow row in table.Rows) // никакого подключения к БД уже нет
|
||||
{
|
||||
int id = (int)row["Id"];
|
||||
string name = (string)row["Name"];
|
||||
decimal price = (decimal)row["Price"];
|
||||
Console.WriteLine($"{id}\t{name}\t{price:C}");
|
||||
}
|
||||
```
|
||||
|
||||
> **Когда что использовать?**
|
||||
> * `SqlDataReader` — когда данных много (тысячи строк) и нужна экономия памяти
|
||||
> и максимальная скорость.
|
||||
> * `SqlDataAdapter` + `DataTable` — когда данных немного и с ними нужно удобно
|
||||
> работать: сортировать, фильтровать, показывать в отчёте, передавать дальше.
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `SqlTransaction` — транзакция
|
||||
|
||||
**Транзакция** — это группа SQL-операций, которые выполняются как одно целое:
|
||||
**всё или ничего**.
|
||||
|
||||
Классический пример из нашего проекта — **создание заказа**. Это несколько операций:
|
||||
|
||||
```
|
||||
1. Создать заказ в таблице Orders
|
||||
2. Для каждого товара заказа:
|
||||
2.1 Уменьшить остаток в таблице Products
|
||||
2.2 Добавить позицию в таблицу OrderItems
|
||||
3. Зафиксировать (COMMIT)
|
||||
```
|
||||
|
||||
Если на шаге 2.1 не хватает товара, то нельзя оставлять базу в таком состоянии:
|
||||
заказ создан, а половина позиций — нет. Нужно отменить **всё**. Именно это
|
||||
и обеспечивает транзакция.
|
||||
|
||||
Реализация в проекте (`AdoNetShop/Data/OrderRepository.cs`):
|
||||
|
||||
```csharp
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
|
||||
// 1) Начинаем транзакцию
|
||||
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 2) Создаём заказ
|
||||
await using var insertOrder = new SqlCommand(insertOrderQuery, connection, transaction);
|
||||
insertOrder.Parameters.Add("@CustomerId", SqlDbType.Int).Value = order.CustomerId;
|
||||
int newOrderId = Convert.ToInt32(await insertOrder.ExecuteScalarAsync());
|
||||
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
// 3) Уменьшаем остаток. В условии — проверка, что товара хватает:
|
||||
// WHERE Id = @ProductId AND StockQuantity >= @Quantity
|
||||
await using var updateStock = new SqlCommand(updateStockQuery, connection, transaction);
|
||||
updateStock.Parameters.Add("@ProductId", SqlDbType.Int).Value = item.ProductId;
|
||||
updateStock.Parameters.Add("@Quantity", SqlDbType.Int).Value = item.Quantity;
|
||||
|
||||
int rowsAffected = await updateStock.ExecuteNonQueryAsync();
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
// Товара не хватает (или его нет). Бросаем исключение —
|
||||
// код перейдёт в catch и сделает откат всей транзакции.
|
||||
throw new Exception($"Недостаточно товара на складе для ProductId: {item.ProductId}");
|
||||
}
|
||||
|
||||
// 4) Добавляем позицию заказа
|
||||
await using var insertItem = new SqlCommand(insertItemQuery, connection, transaction);
|
||||
// ... параметры ...
|
||||
await insertItem.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// 5) Всё прошло успешно — фиксируем результат
|
||||
await transaction.CommitAsync();
|
||||
return newOrderId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 6) Любая ошибка — откатываем все изменения
|
||||
await transaction.RollbackAsync();
|
||||
throw new Exception($"Ошибка при создании заказа. Транзакция отменена. Детали: {ex.Message}", ex);
|
||||
}
|
||||
```
|
||||
|
||||
Обратите внимание на важную деталь: **каждая команда внутри транзакции получает
|
||||
третий параметр — `transaction`**. Если забыть его передать, SQL Server выдаст ошибку
|
||||
«Invalid operation. The transaction associated with this command is not the connection's
|
||||
active transaction».
|
||||
|
||||
---
|
||||
|
||||
## 4. Connected vs Disconnected: в чём разница
|
||||
|
||||
| | Connected (подключенный) | Disconnected (автономный) |
|
||||
|---|---|---|
|
||||
| Что используем | `SqlConnection` + `SqlCommand` + `SqlDataReader` | `SqlDataAdapter` + `DataTable` / `DataSet` |
|
||||
| Когда открыто подключение | Всё время, пока читаем данные | Только на момент загрузки данных |
|
||||
| Как читаем | По одной строке (`ReadAsync()`) | Сразу таблица целиком |
|
||||
| Расход памяти | Минимальный (строка за строкой) | Вся выборка в памяти |
|
||||
| Скорость на больших объёмах | Выше | Ниже (из-за накладных расходов на загрузку) |
|
||||
| Можно работать без БД после загрузки | Нет | Да |
|
||||
| Где в проекте | `GetAllAsync()`, `GetByIdAsync()`, `CreateOrderWithTransactionAsync()` | `GetProductsTableAsync()` (пункт меню 10) |
|
||||
|
||||
Простое правило: **данных много → `SqlDataReader`; данных мало и нужно удобно
|
||||
работать в памяти → `SqlDataAdapter` + `DataTable`.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Структура проекта
|
||||
|
||||
```
|
||||
Artem/
|
||||
│
|
||||
├── README.md ← этот файл (теория + инструкции)
|
||||
├── .gitignore
|
||||
├── AdoNetShop.slnx ← файл решения Visual Studio / Rider
|
||||
│
|
||||
├── AdoNetShop/ ← сам проект C#
|
||||
│ ├── AdoNetShop.csproj ← файл проекта, здесь подключается Microsoft.Data.SqlClient
|
||||
│ ├── Program.cs ← консольное меню и сценарии демонстрации
|
||||
│ ├── appsettings.json ← строка подключения к БД (НЕ в коде!)
|
||||
│ │
|
||||
│ ├── Models/ ← простые классы предметной области (POCO)
|
||||
│ │ ├── Product.cs ← Id, Name, Description, Price, StockQuantity, CategoryId
|
||||
│ │ ├── Category.cs ← Id, Name
|
||||
│ │ ├── Customer.cs ← Id, Name, Email, Phone
|
||||
│ │ ├── Order.cs ← Id, CustomerId, OrderDate, Status, TotalAmount
|
||||
│ │ └── OrderItem.cs ← Id, OrderId, ProductId, Quantity, Price
|
||||
│ │
|
||||
│ ├── Data/ ← ВЕСЬ РАБОТА С БАЗОЙ ЗДЕСЬ (ADO.NET)
|
||||
│ │ ├── DbConnectionFactory.cs ← создание SqlConnection из строки подключения
|
||||
│ │ ├── ProductRepository.cs ← CRUD товаров + SqlDataReader + SqlDataAdapter + Stored Procedure
|
||||
│ │ ├── CategoryRepository.cs ← чтение категорий
|
||||
│ │ ├── CustomerRepository.cs ← чтение клиентов
|
||||
│ │ └── OrderRepository.cs ← создание заказа в транзакции (Commit / Rollback)
|
||||
│ │
|
||||
│ └── Services/
|
||||
│ └── ShopService.cs ← небольшая прослойка: проверки и «прокси» к репозиториям
|
||||
│
|
||||
└── database/ ← SQL-скрипты (выполняются по порядку)
|
||||
├── 01_create_database.sql ← создание базы данных AdoNetShop
|
||||
├── 02_create_tables.sql ← таблицы, ключи, ограничения CHECK, индексы
|
||||
├── 03_insert_data.sql ← тестовые данные
|
||||
├── 04_queries.sql ← примеры запросов
|
||||
├── 05_procedures.sql ← хранимые процедуры
|
||||
└── 06_test.sql ← проверочные запросы (выполнять вручную)
|
||||
```
|
||||
|
||||
**Почему такая структура?** Это классическое разделение ответственности:
|
||||
|
||||
* **Models** — «что есть в магазине». Никакой логики, только данные.
|
||||
* **Data (Repository)** — «как получить и сохранить данные». Здесь живёт
|
||||
весь ADO.NET: `SqlConnection`, `SqlCommand`, `SqlDataReader`, транзакции.
|
||||
* **Services** — «что можно сделать с магазином». Здесь простые проверки
|
||||
данных перед сохранением. Бизнес-логика намеренно не усложнена.
|
||||
* **Program** — «как это показываем пользователю». Консольное меню.
|
||||
|
||||
Repository (репозиторий) — просто класс, в котором собраны SQL-запросы к одной
|
||||
таблице. Это удобно: когда понадобится поменять SQL, вы открываете один файл.
|
||||
|
||||
---
|
||||
|
||||
## 6. Структура базы данных
|
||||
|
||||
```
|
||||
Categories (категории)
|
||||
Id INT IDENTITY PRIMARY KEY
|
||||
Name NVARCHAR(100) NOT NULL
|
||||
▲
|
||||
│ FK Products.CategoryId
|
||||
│
|
||||
Products (товары) ─┐
|
||||
Id INT IDENTITY PRIMARY KEY│ 1 товар ──► много позиций заказа
|
||||
Name NVARCHAR(200) NOT NULL │
|
||||
Description NVARCHAR(MAX) NULL │
|
||||
Price DECIMAL(18,2) NOT NULL CHECK (Price > 0)
|
||||
StockQuantity INT NOT NULL CHECK (StockQuantity >= 0)
|
||||
CategoryId INT NOT NULL │
|
||||
▲ │
|
||||
│ │ FK OrderItems.ProductId
|
||||
│ │
|
||||
Customers (клиенты) │
|
||||
Id INT IDENTITY PRIMARY KEY │
|
||||
Name NVARCHAR(100) NOT NULL │
|
||||
Email NVARCHAR(100) NOT NULL UNIQUE │
|
||||
Phone NVARCHAR(20) NULL │
|
||||
▲ │
|
||||
│ FK Orders.CustomerId │
|
||||
│ │
|
||||
Orders (заказы) ─────────────────────┘
|
||||
Id INT IDENTITY PRIMARY KEY
|
||||
CustomerId INT NOT NULL
|
||||
OrderDate DATETIME2 NOT NULL DEFAULT GETDATE()
|
||||
Status NVARCHAR(50) NOT NULL DEFAULT 'New'
|
||||
TotalAmount DECIMAL(18,2) NOT NULL CHECK (TotalAmount >= 0)
|
||||
|
||||
OrderItems (позиции заказа)
|
||||
Id INT IDENTITY PRIMARY KEY
|
||||
OrderId INT NOT NULL FK Orders(Id) ON DELETE CASCADE
|
||||
ProductId INT NOT NULL FK Products(Id)
|
||||
Quantity INT NOT NULL CHECK (Quantity > 0)
|
||||
Price DECIMAL(18,2) NOT NULL CHECK (Price > 0)
|
||||
```
|
||||
|
||||
Что здесь важно и почему:
|
||||
|
||||
* `IDENTITY` — автоинкремент: база сама генерирует `Id`, приложение его не придумывает.
|
||||
* `PRIMARY KEY` — гарантирует уникальность строк.
|
||||
* `FOREIGN KEY` — гарантирует, что нельзя создать заказ для несуществующего клиента
|
||||
или позицию для несуществующего товара. База защищает целостность данных.
|
||||
* `CHECK` — ограничение на значения: цена не может быть отрицательной,
|
||||
количество в заказе — нулевым или отрицательным.
|
||||
* `UNIQUE` на `Customers.Email` — нельзя завести двух клиентов с одним email.
|
||||
* `INDEX` на внешних ключах (`IX_Products_CategoryId`, `IX_Orders_CustomerId`,
|
||||
`IX_OrderItems_OrderId`, `IX_OrderItems_ProductId`) — ускоряют `JOIN` и поиск.
|
||||
* `DECIMAL(18,2)` для денег — **не** `FLOAT` и **не** `DOUBLE`: это единственный
|
||||
тип, который хранит денежные значения без ошибок округления.
|
||||
* `NVARCHAR`, а не `VARCHAR` — `NVARCHAR` поддерживает кириллицу и любые языки.
|
||||
|
||||
---
|
||||
|
||||
## 7. Как установить SQL Server
|
||||
|
||||
Есть три бесплатных варианта — выбирайте любой:
|
||||
|
||||
### Вариант А: SQL Server Express (самый простой для учёбы)
|
||||
1. Скачайте **SQL Server Express** с официального сайта Microsoft:
|
||||
https://www.microsoft.com/ru-ru/products/sql-server/sql-server-express-downloads
|
||||
2. При установке отметьте **Basic** (базовый) тип установки.
|
||||
3. В мастере установки обязательно включите **средства управления** —
|
||||
**SQL Server Management Tools (SSMS)**. Это программа, в которой вы будете
|
||||
выполнять SQL-скрипты.
|
||||
4. После установки запустите **SQL Server Configuration Manager** и проверьте,
|
||||
что служба **SQL Server (SQLEXPRESS)** запущена (состояние «Running»).
|
||||
5. Откройте SSMS и подключитесь к серверу. Сервер для Express по умолчанию
|
||||
называется **`.\SQLEXPRESS`**.
|
||||
|
||||
### Вариант Б: Developer Edition (полная версия, бесплатно для разработки)
|
||||
Скачивается на той же странице Microsoft. После установки сервер по умолчанию
|
||||
называется **`localhost`** (или имя вашего компьютера).
|
||||
|
||||
### Вариант В: Docker (если установлен Docker)
|
||||
```bash
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Your_password123!" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
|
||||
```
|
||||
|
||||
> **Важно:** SQL Server — это отдельная программа-сервер. Пока он не установлен
|
||||
> и не запущен, приложение не сможет подключиться, даже если код написан правильно.
|
||||
|
||||
---
|
||||
|
||||
## 8. Как создать базу данных и выполнить SQL-скрипты
|
||||
|
||||
### Способ 1: через SSMS (рекомендуется для учебной работы)
|
||||
|
||||
1. Запустите **SQL Server Management Studio**.
|
||||
2. В поле «Тип сервера» выберите **Database Engine**.
|
||||
3. В поле «Имя сервера» введите `localhost` (или `.\SQLEXPRESS` для Express,
|
||||
или `localhost\SQLEXPRESS` — этот вариант подходит всегда).
|
||||
4. Аутентификация: **Windows Authentication** — вводить логин и пароль не нужно.
|
||||
5. Нажмите **Connect**. Если увидели окно с деревом серверов — вы подключились.
|
||||
|
||||
Дальше откройте каждый файл из папки `database` через **File → Open → File**
|
||||
и нажмите **Execute** (или клавишу `F5`). **Порядок важен**, выполняйте строго
|
||||
по номерам:
|
||||
|
||||
| Шаг | Файл | Что делает |
|
||||
|---|---|---|
|
||||
| 1 | `01_create_database.sql` | Создаёт базу данных `AdoNetShop` |
|
||||
| 2 | `02_create_tables.sql` | Создаёт таблицы, ключи, ограничения, индексы |
|
||||
| 3 | `03_insert_data.sql` | Заполняет таблицы тестовыми данными |
|
||||
| 4 | `04_queries.sql` | Примеры запросов (для понимания, можно не выполнять) |
|
||||
| 5 | `05_procedures.sql` | Создаёт хранимые процедуры |
|
||||
| 6 | `06_test.sql` | Проверочные запросы (для самопроверки, можно не выполнять) |
|
||||
|
||||
Скрипты написаны «безопасно»: `01` использует `IF NOT EXISTS`, `02` создаёт
|
||||
таблицы только если их нет, `05` удаляет процедуру перед созданием. Поэтому
|
||||
скрипты можно запускать повторно — ошибки «уже существует» не будет.
|
||||
|
||||
### Способ 2: через командную строку (sqlcmd)
|
||||
|
||||
```bash
|
||||
# Способ с Windows-аутентификацией
|
||||
sqlcmd -S localhost -E -i "database\01_create_database.sql"
|
||||
sqlcmd -S localhost -E -d AdoNetShop -i "database\02_create_tables.sql"
|
||||
sqlcmd -S localhost -E -d AdoNetShop -i "database\03_insert_data.sql"
|
||||
|
||||
# Для SQL Server Express
|
||||
sqlcmd -S .\SQLEXPRESS -E -i "database\01_create_database.sql"
|
||||
```
|
||||
|
||||
### Проверка, что всё создалось
|
||||
|
||||
В SSMS подключитесь к базе `AdoNetShop` (в списке слева) и выполните:
|
||||
|
||||
```sql
|
||||
SELECT * FROM Categories;
|
||||
SELECT * FROM Products;
|
||||
SELECT * FROM Customers;
|
||||
```
|
||||
|
||||
Вы должны увидеть: 3 категории, 6 товаров, 2 клиентов.
|
||||
|
||||
---
|
||||
|
||||
## 9. Как изменить строку подключения (Connection String)
|
||||
|
||||
Строка подключения **не зашита в код**. Она лежит в файле
|
||||
`AdoNetShop/appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Читает её класс `DbConnectionFactory` (`AdoNetShop/Data/DbConnectionFactory.cs`):
|
||||
|
||||
```csharp
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
|
||||
_connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
```
|
||||
|
||||
### Что означают параметры строки подключения
|
||||
|
||||
| Параметр | Значение в примере | Что это |
|
||||
|---|---|---|
|
||||
| `Server` | `localhost` | Адрес сервера SQL Server |
|
||||
| `Database` | `AdoNetShop` | Имя базы данных |
|
||||
| `Trusted_Connection` | `True` | Использовать Windows-аутентификацию (логин/пароль не нужны) |
|
||||
| `TrustServerCertificate` | `True` | Доверять самоподписанному SSL-сертификату SQL Server (нужно для свежих версий `Microsoft.Data.SqlClient`) |
|
||||
|
||||
### Если SQL Server работает как **именованный экземпляр**
|
||||
|
||||
Так бывает после установки SQL Server Express — сервер называется `SQLEXPRESS`.
|
||||
В строке подключения именованный экземпляр пишется через обратный слэш `\`:
|
||||
|
||||
```json
|
||||
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
```
|
||||
|
||||
> **Про обратный слэш и JSON.** В JSON символ `\` — начало escape-последовательности,
|
||||
> поэтому обратный слэш записывается **двойным**: `\\`.
|
||||
> В строке подключения это означает один-единственный обратный слэш.
|
||||
|
||||
Варианты, которые тоже работают:
|
||||
|
||||
```
|
||||
Server=localhost\SQLEXPRESS — если хотите быть явными
|
||||
Server=(local)\SQLEXPRESS — эквивалент
|
||||
Server=.\MSSQLSERVER — именованный экземпляр основного сервера
|
||||
```
|
||||
|
||||
### Если нужна аутентификация SQL Server (логин и пароль)
|
||||
|
||||
```json
|
||||
"DefaultConnection": "Server=localhost;Database=AdoNetShop;User Id=sa;Password=ТвойПароль;TrustServerCertificate=True;"
|
||||
```
|
||||
|
||||
> Внимание: пароль в `appsettings.json` — это «учебный» вариант. В настоящем проекте
|
||||
> такие данные хранят в переменных окружения или в защищённом хранилище секретов.
|
||||
> Для лабораторной работы достаточно `appsettings.json` — главное, что пароль
|
||||
> **не зашит в исходный код**, а вынесен в конфигурацию.
|
||||
|
||||
### Как проверить, что подключение работает
|
||||
|
||||
```bash
|
||||
# В SSMS: Server → Connect → Database Engine, имя сервера как в appsettings.json
|
||||
# Либо в командной строке проекта (Visual Studio / Rider → Terminal):
|
||||
cd AdoNetShop
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Если в меню выбрать пункт `1. Список товаров` и вы увидите таблицу с товарами —
|
||||
подключение работает корректно.
|
||||
|
||||
---
|
||||
|
||||
## 10. Как запустить проект
|
||||
|
||||
### Что нужно
|
||||
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) — проверьте командой `dotnet --version`
|
||||
* Запущенный SQL Server (см. раздел 7)
|
||||
* Выполненные скрипты `01`, `02`, `03`, `05` (см. раздел 8)
|
||||
* Правильная строка подключения в `appsettings.json` (см. раздел 9)
|
||||
|
||||
### Запуск из командной строки
|
||||
|
||||
```bash
|
||||
cd AdoNetShop
|
||||
|
||||
# Восстановить пакеты (обычно делается автоматически)
|
||||
dotnet restore
|
||||
|
||||
# Собрать проект
|
||||
dotnet build
|
||||
|
||||
# Запустить
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Запуск из Visual Studio
|
||||
|
||||
1. Откройте файл решения `AdoNetShop.slnx` (или `AdoNetShop.csproj`).
|
||||
2. Убедитесь, что в качестве стартового проекта выбран `AdoNetShop`.
|
||||
3. Нажмите `Ctrl + F5` (запуск без отладки) или `F5` (с отладкой).
|
||||
4. Для отладки поставьте точки останова в `ProductRepository.GetAllAsync` и
|
||||
`OrderRepository.CreateOrderWithTransactionAsync` — очень полезно для защиты.
|
||||
|
||||
### Запуск из Rider
|
||||
|
||||
Откройте папку проекта в Rider — он сам определит `.csproj`. Нажмите `Shift + F10`.
|
||||
|
||||
### Что должно появиться
|
||||
|
||||
```
|
||||
================================
|
||||
ADO.NET SHOP
|
||||
================================
|
||||
1. Список товаров
|
||||
2. Найти товар по ID
|
||||
3. Добавить товар
|
||||
4. Изменить товар
|
||||
5. Удалить товар
|
||||
6. Список категорий
|
||||
7. Список клиенты
|
||||
8. Создать заказ (Демонстрация Transaction)
|
||||
9. Просмотреть заказы
|
||||
10. Демонстрация DataAdapter (Disconnected mode)
|
||||
11. Демонстрация Stored Procedures
|
||||
0. Выход
|
||||
================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Что умеет программа (демонстрация преподавателю)
|
||||
|
||||
Каждый пункт меню — это отдельная демонстрация элемента ADO.NET.
|
||||
|
||||
### Пункт 1. Список товаров → `SqlDataReader` + `ExecuteReaderAsync`
|
||||
|
||||
Вызывает `ProductRepository.GetAllAsync()`. Показывает потоковое чтение
|
||||
результата `SELECT` через `SqlDataReader`.
|
||||
|
||||
**Что говорить:** «Открывается соединение, выполняется SELECT, и результат
|
||||
читается построчно объектом `SqlDataReader`, пока `ReadAsync()` возвращает `true`».
|
||||
|
||||
### Пункт 2. Найти товар по ID → параметризованный `SqlCommand`
|
||||
|
||||
Вызывает `GetByIdAsync(id)`. Демонстрирует `WHERE Id = @Id` + `SqlParameter`.
|
||||
|
||||
**Что говорить:** «Пользовательский ввод никогда не подставляется в текст запроса
|
||||
через конкатенацию — только через параметры. Это защита от SQL Injection».
|
||||
|
||||
### Пункт 3. Добавить товар → `ExecuteScalarAsync`
|
||||
|
||||
Вызывает `CreateAsync(product)`. Вставляет строку и получает сгенерированный
|
||||
базой `Id` через `SELECT SCOPE_IDENTITY()`.
|
||||
|
||||
**Что говорить:** «`ExecuteScalarAsync` возвращает одно значение — в нашем случае
|
||||
это `Id` нового товара, который сгенерировал `IDENTITY`».
|
||||
|
||||
### Пункт 4. Изменить товар → `ExecuteNonQueryAsync`
|
||||
|
||||
Вызывает `UpdateAsync(product)`. Возвращает количество затронутых строк.
|
||||
|
||||
### Пункт 5. Удалить товар → `ExecuteNonQueryAsync`
|
||||
|
||||
Вызывает `DeleteAsync(id)`. Обратите внимание: если на товар ссылаются строки
|
||||
в `OrderItems`, база не даст удалить такой товар (Foreign Key). Это тоже можно
|
||||
показать как защиту целостности данных.
|
||||
|
||||
### Пункт 6. Категории / Пункт 7. Клиенты → простые `SELECT`
|
||||
|
||||
Чтение справочных таблиц, которые нужны, чтобы заполнить выпадающие списки
|
||||
при добавлении товара и оформлении заказа.
|
||||
|
||||
### Пункт 8. Создать заказ → `SqlTransaction`, `Commit`, `Rollback` ★
|
||||
|
||||
**Главная демонстрация лабораторной.** Создаёт заказ целиком в одной транзакции.
|
||||
|
||||
**Как проверить откат:**
|
||||
1. Выберите клиента (например, `1`).
|
||||
2. Добавьте товар `1` в количестве `2` — заказ создастся успешно.
|
||||
3. Добавьте товар, которого на складе мало, в количестве `9999`.
|
||||
4. Программа выведет ошибку «Недостаточно товара на складе».
|
||||
5. Откройте SSMS и выполните `SELECT * FROM Orders;` — нового заказа не будет.
|
||||
Транзакция откатила **всё**: и заказ, и позиции, и изменения остатков.
|
||||
|
||||
**Что говорить:** «Транзакция гарантирует принцип "все или ничего". Нельзя получить
|
||||
ситуацию, когда заказ создан, а товары к нему — нет».
|
||||
|
||||
### Пункт 9. Просмотреть заказы → чтение результата
|
||||
|
||||
Показывает созданные заказы: клиент, сумма, дата, статус.
|
||||
|
||||
### Пункт 10. DataAdapter demo → `SqlDataAdapter` + `DataTable` (disconnected)
|
||||
|
||||
Вызывает `GetProductsTableAsync()`. Показывает, что данные загрузились
|
||||
в таблицу в памяти и с ними можно работать **без подключения к базе**.
|
||||
|
||||
**Что говорить:** «`SqlDataAdapter` сам открыл подключение, скачал данные в
|
||||
`DataTable` и закрыл подключение. Дальше мы работаем с обычной таблицей в памяти —
|
||||
база данных в этот момент свободна для других пользователей».
|
||||
|
||||
### Пункт 11. Stored Procedures demo → `CommandType.StoredProcedure`
|
||||
|
||||
Вызывает хранимые процедуры `dbo.GetProducts` и `dbo.UpdateProductPrice`.
|
||||
|
||||
**Как проверить обработку ошибки:** введите цену `-5` — процедура вызовет
|
||||
`RAISERROR`, приложение перехватит `SqlException` и покажет сообщение.
|
||||
|
||||
**Что говорить:** «Хранимые процедуры — это SQL-код, который хранится на сервере.
|
||||
Из C# мы вызываем их по имени, а не пишем запрос текстом. Основная бизнес-логика
|
||||
при этом остаётся в приложении — процедуры здесь только для демонстрации».
|
||||
|
||||
---
|
||||
|
||||
## 12. Обработка ошибок и валидация
|
||||
|
||||
### Проверка пользовательского ввода (в `Program.cs` и `ShopService.cs`)
|
||||
|
||||
| Что проверяем | Где |
|
||||
|---|---|
|
||||
| ID — целое число | `int.TryParse(...)` перед вызовом репозитория |
|
||||
| Цена больше нуля | `ShopService.CreateProductAsync`, `Product.Price` |
|
||||
| Количество на складе не отрицательное | `ShopService.CreateProductAsync`, `StockQuantity` |
|
||||
| Email не пустой | при добавлении клиента |
|
||||
| Количество товара в заказе больше нуля | при оформлении заказа |
|
||||
| Товар существует и его хватает | `ShopService.CreateOrderAsync` до открытия транзакции |
|
||||
|
||||
Пример:
|
||||
|
||||
```csharp
|
||||
if (product.Price <= 0)
|
||||
throw new ArgumentException("Price must be greater than 0");
|
||||
if (product.StockQuantity < 0)
|
||||
throw new ArgumentException("Stock cannot be negative");
|
||||
```
|
||||
|
||||
### Обработка ошибок в приложении
|
||||
|
||||
В главном цикле `Program.cs` все пункты меню выполняются внутри `try/catch`:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
switch (input) { /* ... */ }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ОШИБКА]: {ex.Message}");
|
||||
if (ex.InnerException != null)
|
||||
Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
Такой подход выбран сознательно: **для учебного проекта исключения не скрываются
|
||||
молча** — пользователь видит, что произошло. В продакшене ошибки логировали бы
|
||||
в файл, а пользователю показывали бы нейтральное сообщение.
|
||||
|
||||
Дополнительно ошибки от самой СУБД обрабатываются отдельно. `SqlException` —
|
||||
это исключение SQL Server, в нём есть номер ошибки и текст от сервера:
|
||||
|
||||
```csharp
|
||||
catch (SqlException ex)
|
||||
{
|
||||
Console.WriteLine($"Ошибка SQL Server (код {ex.Number}): {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Какие файлы нужно изучить в первую очередь
|
||||
|
||||
Если преподаватель спросит «покажи, где у тебя ADO.NET», открывайте эти файлы
|
||||
в таком порядке:
|
||||
|
||||
### 1. `AdoNetShop/Data/DbConnectionFactory.cs` — подключение
|
||||
Класс, который создаёт `SqlConnection`. Здесь видно, что строка подключения
|
||||
берётся из `appsettings.json`.
|
||||
|
||||
### 2. `AdoNetShop/Data/ProductRepository.cs` — основные элементы ADO.NET
|
||||
**Самый важный файл лабораторной.** В нём собрано всё сразу:
|
||||
|
||||
| Метод | Что показывает |
|
||||
|---|---|
|
||||
| `GetAllAsync()` | `SqlConnection.OpenAsync()`, `SqlCommand`, `ExecuteReaderAsync()`, `SqlDataReader` |
|
||||
| `GetByIdAsync(id)` | `SqlParameter`, параметризованный запрос |
|
||||
| `CreateAsync(product)` | `ExecuteScalarAsync()`, `SCOPE_IDENTITY()` |
|
||||
| `UpdateAsync(product)` | `ExecuteNonQueryAsync()` для `UPDATE` |
|
||||
| `DeleteAsync(id)` | `ExecuteNonQueryAsync()` для `DELETE` |
|
||||
| `GetProductsTableAsync()` | `SqlDataAdapter` + `DataTable` (disconnected) |
|
||||
| `GetProductsByStoredProcedureAsync(...)` | `CommandType.StoredProcedure` |
|
||||
| `UpdatePriceViaStoredProcedureAsync(...)` | `StoredProcedure` + `UPDATE`, `SqlException` |
|
||||
|
||||
### 3. `AdoNetShop/Data/OrderRepository.cs` — транзакция
|
||||
Метод `CreateOrderWithTransactionAsync()`: `BeginTransactionAsync()` → `CommitAsync()`
|
||||
/ `RollbackAsync()`. Показывает, что все команды внутри транзакции получают
|
||||
третий параметр `transaction`.
|
||||
|
||||
### 4. `AdoNetShop/appsettings.json` — конфигурация
|
||||
Строка подключения вынесена из кода. Покажите преподавателю, что в исходниках
|
||||
нет ни логинов, ни паролей.
|
||||
|
||||
### 5. `database/02_create_tables.sql` — схема БД
|
||||
`PRIMARY KEY`, `FOREIGN KEY`, `CHECK`, индексы. Свяжите таблицы с классами
|
||||
из папки `Models/`.
|
||||
|
||||
### 6. `database/05_procedures.sql` — хранимые процедуры
|
||||
Три процедуры, которые вызываются из `ProductRepository`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Частые проблемы и их решения
|
||||
|
||||
| Симптом | Причина | Что сделать |
|
||||
|---|---|---|
|
||||
| `A network-related or instance-specific error occurred` | SQL Server не запущен или неверный адрес сервера | Запустите службу SQL Server. Проверьте `Server` в `appsettings.json`. Для Express должно быть `.\SQLEXPRESS` |
|
||||
| `Login failed for user` | Неверный режим аутентификации | Попробуйте `Trusted_Connection=True` (Windows Auth) вместо `User Id=sa;Password=...` |
|
||||
| `Certificate is not trusted` | Не настроен SSL-сертификат SQL Server | Добавьте `TrustServerCertificate=True` в строку подключения |
|
||||
| `Cannot open database "AdoNetShop"` | База не создана | Выполните `01_create_database.sql` в SSMS |
|
||||
| `Invalid object name 'Products'` | Таблицы не созданы | Выполните `02_create_tables.sql` |
|
||||
| `Could not find file appsettings.json` | Запустили из другой папки | Запускайте команду из папки `AdoNetShop`, где лежит `.csproj` |
|
||||
| `The model ... context has been changed` | Изменили модель после миграции | Не применимо к этому проекту — здесь **нет** EF и миграций |
|
||||
| Ошибка `PK_...` при `INSERT` | Попытка вставить дубликат | Например, два клиента с одинаковым Email (`UNIQUE`) |
|
||||
| Не компилируется проект | Не установлен .NET 8 SDK | Установите SDK и выполните `dotnet restore` |
|
||||
|
||||
---
|
||||
|
||||
## Краткая памятка: 12 элементов ADO.NET, которые демонстрирует проект
|
||||
|
||||
| # | Элемент ADO.NET | Где найти |
|
||||
|---|---|---|
|
||||
| 1 | `SqlConnection` (создание) | `Data/DbConnectionFactory.cs` |
|
||||
| 2 | `OpenAsync()` / `Close()` / `using` | все репозитории |
|
||||
| 3 | `SqlCommand` | `Data/ProductRepository.cs` |
|
||||
| 4 | `ExecuteReaderAsync()` | `GetAllAsync` |
|
||||
| 5 | `ExecuteScalarAsync()` | `CreateAsync` |
|
||||
| 6 | `ExecuteNonQueryAsync()` | `UpdateAsync`, `DeleteAsync` |
|
||||
| 7 | `SqlParameter` | `GetByIdAsync`, `CreateAsync` |
|
||||
| 8 | `SqlDataReader` | `GetAllAsync` |
|
||||
| 9 | `SqlDataAdapter` + `DataTable` | `GetProductsTableAsync` |
|
||||
| 10 | `SqlTransaction`, `Commit`, `Rollback` | `Data/OrderRepository.cs` |
|
||||
| 11 | `CommandType.StoredProcedure` | `GetProductsByStoredProcedureAsync` |
|
||||
| 12 | `SqlException` | `UpdatePriceViaStoredProcedureAsync` |
|
||||
|
||||
---
|
||||
|
||||
*Проект создан в учебных целях для лабораторной работы по технологии ADO.NET.
|
||||
ADO.NET используется как основной и единственный способ работы с базой данных.*
|
||||
11
database/01_create_database.sql
Normal file
11
database/01_create_database.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- 01_create_database.sql
|
||||
-- Скрипт для создания базы данных
|
||||
|
||||
USE master;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'AdoNetShop')
|
||||
BEGIN
|
||||
CREATE DATABASE [AdoNetShop];
|
||||
END
|
||||
GO
|
||||
85
database/02_create_tables.sql
Normal file
85
database/02_create_tables.sql
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
-- 02_create_tables.sql
|
||||
-- Скрипт для создания таблиц
|
||||
|
||||
USE [AdoNetShop];
|
||||
GO
|
||||
|
||||
-- Таблица Categories
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Categories')
|
||||
BEGIN
|
||||
CREATE TABLE Categories (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Таблица Products
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Products')
|
||||
BEGIN
|
||||
CREATE TABLE Products (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(200) NOT NULL,
|
||||
Description NVARCHAR(MAX) NULL,
|
||||
Price DECIMAL(18, 2) NOT NULL CHECK (Price > 0),
|
||||
StockQuantity INT NOT NULL DEFAULT 0 CHECK (StockQuantity >= 0),
|
||||
CategoryId INT NOT NULL,
|
||||
FOREIGN KEY (CategoryId) REFERENCES Categories(Id)
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Таблица Customers
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Customers')
|
||||
BEGIN
|
||||
CREATE TABLE Customers (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL,
|
||||
Email NVARCHAR(100) NOT NULL UNIQUE,
|
||||
Phone NVARCHAR(20) NULL
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Таблица Orders
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Orders')
|
||||
BEGIN
|
||||
CREATE TABLE Orders (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerId INT NOT NULL,
|
||||
OrderDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
Status NVARCHAR(50) NOT NULL DEFAULT 'New',
|
||||
TotalAmount DECIMAL(18, 2) NOT NULL DEFAULT 0 CHECK (TotalAmount >= 0),
|
||||
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Таблица OrderItems
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'OrderItems')
|
||||
BEGIN
|
||||
CREATE TABLE OrderItems (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderId INT NOT NULL,
|
||||
ProductId INT NOT NULL,
|
||||
Quantity INT NOT NULL CHECK (Quantity > 0),
|
||||
Price DECIMAL(18, 2) NOT NULL CHECK (Price > 0),
|
||||
FOREIGN KEY (OrderId) REFERENCES Orders(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ProductId) REFERENCES Products(Id)
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Индексы для внешних ключей (улучшают производительность JOIN'ов)
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Products_CategoryId')
|
||||
CREATE INDEX IX_Products_CategoryId ON Products(CategoryId);
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Orders_CustomerId')
|
||||
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_OrderItems_OrderId')
|
||||
CREATE INDEX IX_OrderItems_OrderId ON OrderItems(OrderId);
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_OrderItems_ProductId')
|
||||
CREATE INDEX IX_OrderItems_ProductId ON OrderItems(ProductId);
|
||||
GO
|
||||
42
database/03_insert_data.sql
Normal file
42
database/03_insert_data.sql
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- 03_insert_data.sql
|
||||
-- Скрипт для вставки тестовых данных
|
||||
|
||||
USE [AdoNetShop];
|
||||
GO
|
||||
|
||||
-- Очистка таблиц перед вставкой (опционально, для чистоты эксперимента)
|
||||
-- Отключаем проверку внешних ключей
|
||||
EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'
|
||||
EXEC sp_MSForEachTable 'DELETE FROM ?'
|
||||
EXEC sp_MSForEachTable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL'
|
||||
-- Сброс identity
|
||||
DBCC CHECKIDENT ('Categories', RESEED, 0);
|
||||
DBCC CHECKIDENT ('Products', RESEED, 0);
|
||||
DBCC CHECKIDENT ('Customers', RESEED, 0);
|
||||
DBCC CHECKIDENT ('Orders', RESEED, 0);
|
||||
DBCC CHECKIDENT ('OrderItems', RESEED, 0);
|
||||
GO
|
||||
|
||||
|
||||
-- Добавляем категории
|
||||
INSERT INTO Categories (Name) VALUES
|
||||
('Laptops'),
|
||||
('Smartphones'),
|
||||
('Accessories');
|
||||
|
||||
-- Добавляем товары
|
||||
INSERT INTO Products (Name, Description, Price, StockQuantity, CategoryId) VALUES
|
||||
('Laptop Dell XPS 15', 'High performance laptop', 1500.00, 10, 1),
|
||||
('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
|
||||
28
database/04_queries.sql
Normal file
28
database/04_queries.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- 04_queries.sql
|
||||
-- Примеры запросов
|
||||
|
||||
USE [AdoNetShop];
|
||||
GO
|
||||
|
||||
-- Получить все товары с названиями категорий
|
||||
SELECT
|
||||
p.Id,
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.StockQuantity,
|
||||
c.Name AS CategoryName
|
||||
FROM Products p
|
||||
JOIN Categories c ON p.CategoryId = c.Id;
|
||||
|
||||
-- Получить все заказы клиента с деталями
|
||||
SELECT
|
||||
o.Id AS OrderId,
|
||||
o.OrderDate,
|
||||
o.TotalAmount,
|
||||
p.Name AS ProductName,
|
||||
oi.Quantity,
|
||||
oi.Price
|
||||
FROM Orders o
|
||||
JOIN OrderItems oi ON o.Id = oi.OrderId
|
||||
JOIN Products p ON oi.ProductId = p.Id
|
||||
WHERE o.CustomerId = 1;
|
||||
73
database/05_procedures.sql
Normal file
73
database/05_procedures.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- 05_procedures.sql
|
||||
-- Хранимые процедуры (Stored Procedures).
|
||||
-- ВАЖНО: основная бизнес-логика остаётся в C# (ADO.NET).
|
||||
-- Здесь только простые процедуры, чтобы показать, как ADO.NET
|
||||
-- умеет вызывать хранимую процедуру через CommandType.StoredProcedure.
|
||||
|
||||
USE [AdoNetShop];
|
||||
GO
|
||||
|
||||
-- 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 IS NULL OR CategoryId = @CategoryId
|
||||
ORDER BY Name;
|
||||
END
|
||||
GO
|
||||
|
||||
-- 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: сами мы формируем сообщение об ошибке и отдаём его клиенту
|
||||
RAISERROR ('Цена товара должна быть больше нуля.', 16, 1);
|
||||
RETURN;
|
||||
END
|
||||
|
||||
UPDATE Products
|
||||
SET Price = @NewPrice
|
||||
WHERE Id = @ProductId;
|
||||
|
||||
IF @@ROWCOUNT = 0
|
||||
RAISERROR ('Товар с таким Id не найден.', 16, 1);
|
||||
END
|
||||
GO
|
||||
74
database/06_test.sql
Normal file
74
database/06_test.sql
Normal file
|
|
@ -0,0 +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;
|
||||
|
||||
-- ============ Примеры SELECT-запросов из приложения ============
|
||||
|
||||
-- 1. Все товары (демонстрация SqlDataReader)
|
||||
SELECT Id, Name, Description, Price, StockQuantity, CategoryId
|
||||
FROM Products;
|
||||
GO
|
||||
|
||||
-- 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