From 8f381db2cbab8edbda3c4d1daa67be8fc7b3e782 Mon Sep 17 00:00:00 2001 From: Student Date: Mon, 5 Oct 2026 06:41:31 +0500 Subject: [PATCH] Rewrite README in plain language, fix repository description --- AdoNetShop/Data/OrderRepository.cs | 12 +- AdoNetShop/Program.cs | 10 +- README.md | 1366 ++++++++-------------------- 3 files changed, 411 insertions(+), 977 deletions(-) diff --git a/AdoNetShop/Data/OrderRepository.cs b/AdoNetShop/Data/OrderRepository.cs index 69a9a81..42061ca 100644 --- a/AdoNetShop/Data/OrderRepository.cs +++ b/AdoNetShop/Data/OrderRepository.cs @@ -94,7 +94,7 @@ public class OrderRepository // Первая позиция списывается успешно, на второй специально выбрасывается // ошибка. В результате БД не меняется вообще: остатки возвращаются // к прежнему значению, а заказ не создаётся. - // Метод возвращает текст описания случившимся — для вывода в консоль. + // Метод возвращает текст описания случившимся - для вывода в консоль. public async Task CreateOrderWithRollbackDemoAsync(int customerId) { await using var connection = _connectionFactory.CreateConnection(); @@ -123,7 +123,7 @@ public class OrderRepository var report = new List(); report.Add("Остатки ДО операции:"); foreach (var it in demoItems) - report.Add($" [{it.Id}] {it.Name} — было {it.Stock} шт."); + report.Add($" [{it.Id}] {it.Name} - было {it.Stock} шт."); await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync(); @@ -146,12 +146,12 @@ public class OrderRepository await orderCommand.ExecuteNonQueryAsync(); } - // Шаг 2. Списываем первую позицию — это успешно + // Шаг 2. Списываем первую позицию - это успешно await DeductStockAsync(connection, transaction, demoItems[0].Id, 2); report.Add($"Списание товара [{demoItems[0].Id}] {demoItems[0].Name} - 2 шт. прошло успешно."); report.Add("Но тут приложение «падает» (искусственная ошибка)."); - report.Add("Вызывается Rollback — все изменения выше отменяются:"); + report.Add("Вызывается Rollback - все изменения выше отменяются:"); // Шаг 3. Искусственная ошибка внутри транзакции throw new InvalidOperationException("Искусственная ошибка при обработке второй позиции заказа"); @@ -164,7 +164,7 @@ public class OrderRepository report.Add("Выполнен transaction.RollbackAsync()."); } - // Читаем остатки ПОСЛЕ отката — база вернулась в исходное состояние + // Читаем остатки ПОСЛЕ отката - база вернулась в исходное состояние string checkSql = @" SELECT Id, Name, StockQuantity FROM Products @@ -179,7 +179,7 @@ public class OrderRepository { while (await checkReader.ReadAsync()) { - report.Add($" [{checkReader.GetInt32(0)}] {checkReader.GetString(1)} — стало {checkReader.GetInt32(2)} шт."); + report.Add($" [{checkReader.GetInt32(0)}] {checkReader.GetString(1)} - стало {checkReader.GetInt32(2)} шт."); } } diff --git a/AdoNetShop/Program.cs b/AdoNetShop/Program.cs index 9b0445c..a64288b 100644 --- a/AdoNetShop/Program.cs +++ b/AdoNetShop/Program.cs @@ -52,7 +52,7 @@ class Program var input = Console.ReadLine(); - // Ввод закрыт (например, перенаправлен и закончился) — выходим из программы, + // Ввод закрыт (например, перенаправлен и закончился) - выходим из программы, // иначе цикл меню будет крутиться бесконечно. if (input is null) { @@ -128,7 +128,7 @@ var input = Console.ReadLine(); } // Очистка экрана. Console.Clear() не работает, если вывод перенаправлен - // (например, при запуске через管道 или в CI) — в этом случае просто пропускаем её. + // (например, при запуске через管道 или в CI) - в этом случае просто пропускаем её. private static void ClearConsole() { try @@ -137,7 +137,7 @@ var input = Console.ReadLine(); } catch (IOException) { - // Консоли нет — ничего не очищаем + // Консоли нет - ничего не очищаем } } @@ -359,7 +359,7 @@ var input = Console.ReadLine(); Console.WriteLine($"Процедура вернула товаров: {products.Count}"); foreach (var p in products) { - Console.WriteLine($" [{p.Id}] {p.Name} — {p.Price:C} (остаток: {p.StockQuantity})"); + Console.WriteLine($" [{p.Id}] {p.Name} - {p.Price:C} (остаток: {p.StockQuantity})"); } if (products.Count == 0) @@ -385,7 +385,7 @@ var input = Console.ReadLine(); } // Наглядная демонстрация отката транзакции (Rollback). - // Первая позиция списывается успешно, затем происходит ошибка — + // Первая позиция списывается успешно, затем происходит ошибка - // и вся транзакция отменяется. Остатки в базе не меняются. private static async Task DemoRollback(ShopService service) { diff --git a/README.md b/README.md index 627d8e1..3cf539f 100644 --- a/README.md +++ b/README.md @@ -1,629 +1,38 @@ -# AdoNetShop — лабораторная работа по технологии ADO.NET +# AdoNetShop — лабораторная работа по ADO.NET -**Что это за проект.** Это небольшой учебный интернет-магазин, написанный на C# (.NET 8). -Всё общение с базой данных Microsoft SQL Server выполняется **вручную через ADO.NET** -(пакет `Microsoft.Data.SqlClient`). В проекте **нет** Entity Framework, Dapper, NHibernate -и любых других ORM — нам важно увидеть, как приложение общается с БД «на низком уровне». +Небольшой интернет-магазин на C#, который работает с Microsoft SQL Server через ADO.NET. +ORM здесь не используются: ни Entity Framework, ни Dapper. Все запросы к базе написаны +вручную, поэтому в коде видно, как приложение общается с базой данных. ---- +## Что нужно для запуска -## Содержание +1. .NET 8 SDK. Проверить: `dotnet --version` +2. Microsoft SQL Server (Developer, Express или LocalDB). +3. SQL Server Management Studio (SSMS) — программа, в которой выполняют SQL-скрипты. -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. Создайте базу данных.** -## 1. Что такое ADO.NET и зачем он нужен +Откройте SSMS, подключитесь к своему серверу и выполните файлы из папки `database` +строго по порядку: -**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` | +| `01_create_database.sql` | создаёт базу `AdoNetShop` | +| `02_create_tables.sql` | создаёт таблицы, ключи, ограничения, индексы | +| `03_insert_data.sql` | добавляет тестовые данные | +| `05_procedures.sql` | создаёт хранимые процедуры | -В нашем проекте используется **`Microsoft.Data.SqlClient`** — официальный провайдер -для SQL Server. Именно он предоставляет классы `SqlConnection`, `SqlCommand`, -`SqlDataReader`, `SqlDataAdapter`, `SqlTransaction`. +Файлы `04_queries.sql` и `06_test.sql` — это примеры запросов для проверки. +Их можно не выполнять. -> **Зачем провайдер вынесен в отдельный пакет, а не встроен в .NET?** -> Разработчики .NET не могут включить в платформу драйверы ко всем существующим СУБД — -> это слишком много кода и лицензионные ограничения. Поэтому каждый провайдер — -> отдельный NuGet-пакет, который подключается одной строкой в `.csproj`: +Все скрипты можно запускать повторно: старые данные удаляются, новые добавляются +с теми же номерами. -```xml - -``` +**Шаг 2. Проверьте строку подключения.** ---- - -## 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 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` создаёт базу только если её ещё нет; -* `02` создаёт таблицы только если их нет; -* `03` сначала удаляет прежние данные, а затем подставляет свои значения вместе с - `Id` (через `SET IDENTITY_INSERT`), поэтому результат всегда одинаковый: - товары `Id = 1…6`, клиенты `Id = 1…2`; -* `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`: +Файл `AdoNetShop/appsettings.json`: ```json { @@ -633,297 +42,367 @@ SELECT * FROM Customers; } ``` -Читает её класс `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`: ``` -Server=localhost\SQLEXPRESS — если хотите быть явными -Server=(local)\SQLEXPRESS — эквивалент -Server=.\MSSQLSERVER — именованный экземпляр основного сервера +Server=localhost обычный SQL Server +Server=.\\SQLEXPRESS SQL Server Express +Server=(localdb)\\MSSQLLocalDB LocalDB ``` -### Если используется LocalDB (лёгкая версия SQL Server для разработки) +В JSON обратный слэш пишется дважды: `\\` вместо `\`. +Подробнее про каждый параметр строки подключения — в разделе ниже. -LocalDB — это «облегчённый» SQL Server, который запускается одной командой и не требует -установки службы. Он удобен тем, что не занимает ресурсы и не мешает другим программам. - -Запуск экземпляра (по умолчанию он называется `MSSQLLocalDB`): +**Шаг 3. Запустите приложение.** ```bash -sqllocaldb start MSSQLLocalDB -``` - -Строка подключения для него: - -```json -"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;" -``` - -> В файле `appsettings.json` строка по умолчанию — `Server=localhost`. -> Если у вас LocalDB, замените её на вариант выше. - -### Если нужна аутентификация 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. Список товаров` и вы увидите таблицу с товарами — -подключение работает корректно. +**Шаг 4. Пользуйтесь меню.** ---- +Если база создана неправильно или сервер выключен, программа не сломается: +она покажет понятное сообщение об ошибке и продолжит работать. -## 10. Как запустить проект +## Что такое ADO.NET -### Что нужно -* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) — проверьте командой `dotnet --version` -* Запущенный SQL Server (см. раздел 7) -* Выполненные скрипты `01`, `02`, `03`, `05` (см. раздел 8) -* Правильная строка подключения в `appsettings.json` (см. раздел 9) +ADO.NET — это набор классов, через которые программа на C# общается с базой данных. +Без него приложение не знает, как отправить запрос в SQL Server и как получить ответ. -### Запуск из командной строки - -```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 -12. Демонстрация ROLLBACK транзакции -0. Выход -================================ +Приложение на C# <--> ADO.NET <--> SQL Server + наш код переводчик где лежат данные ``` -> **Если .NET 8 не установлен.** Проект собирается под `net8.0`, но в `.csproj` -> включён параметр `LatestMajor`. Это значит: если на -> компьютере установлена только более новая версия .NET (9, 10, 11…), -> приложение всё равно запустится на ней. Если же ошибка всё-таки появилась, -> либо поставьте .NET 8 SDK, либо смените в `AdoNetShop.csproj` строку -> `net8.0` на установленную у вас версию. +### Зачем он нужен -> **Кодировка SQL-файлов.** Все скрипты в папке `database` сохранены в UTF-8 **с BOM** -> (метка порядка байтов в начале файла). Благодаря этому кириллица корректно читается -> и в SSMS, и в Visual Studio, и в командной строке (`sqlcmd -f 65001 -i файл.sql`). -> Если вы пересохраните скрипт в другой кодировке — русские названия могут превратиться -> в «кракозябры». +- Можно писать любой SQL, какой поддерживает база. ORM так не умеет без обходных путей. +- Работает быстро: нет лишних проверок и отслеживания изменений, которые делают ORM. +- Показывает, как устроена работа с базой на самом деле. Любой ORM внутри делает + почти то же самое, только прячет это от вас. ---- +### Что такое провайдер -## 11. Что умеет программа (демонстрация преподавателю) +ADO.NET сам не ходит в базу. Для каждой СУБД есть отдельная библиотека — +это и есть провайдер: -Каждый пункт меню — это отдельная демонстрация элемента 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. Демонстрация ROLLBACK → наглядный откат транзакции ★ - -Второй сценарий, который обязательно стоит показать преподавателю. -Метод `OrderRepository.CreateOrderWithRollbackDemoAsync()` делает ровно то, -что описано в учебнике про транзакции: - -1. Открывает транзакцию (`BeginTransactionAsync`). -2. Создаёт заказ. -3. **Успешно** списывает 2 штуки первого товара. -4. Затем «падает» с ошибкой (искусственное исключение). -5. Выполняет `RollbackAsync()`. -6. Читает остатки заново и показывает их в консоли. - -**Что увидит преподаватель:** первое списание прошло успешно, но после отката -остатки **такие же, как были до операции**, а заказ в базе отсутствует. -Вот эту картинку и нужно доказать: «даже если часть операций внутри транзакции -успела выполниться, откат возвращает базу в исходное состояние». - ---- - -## 12. Обработка ошибок и валидация - -### Проверка пользовательского ввода (в `Program.cs` и `ShopService.cs`) - -| Что проверяем | Где | +| База | Провайдер | |---|---| -| ID — целое число | `int.TryParse(...)` перед вызовом репозитория | -| Цена больше нуля | `ShopService.CreateProductAsync`, `Product.Price` | -| Количество на складе не отрицательное | `ShopService.CreateProductAsync`, `StockQuantity` | -| Email не пустой | при добавлении клиента | -| Количество товара в заказе больше нуля | при оформлении заказа | -| Товар существует и его хватает | `ShopService.CreateOrderAsync` до открытия транзакции | +| SQL Server | `Microsoft.Data.SqlClient` | +| PostgreSQL | `Npgsql` | +| MySQL | `MySqlConnector` | +| SQLite | `Microsoft.Data.Sqlite` | -Пример: +В нашем проекте используется `Microsoft.Data.SqlClient`. Он подключается одной +строкой в файле проекта: -```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"); +```xml + ``` -### Обработка ошибок в приложении +## Главные классы ADO.NET -В главном цикле `Program.cs` все пункты меню выполняются внутри `try/catch`: +### SqlConnection — подключение к базе + +`SqlConnection` — это подключение к серверу. Создаётся из строки подключения. ```csharp +await using var connection = new SqlConnection(connectionString); + +// Открываем подключение +await connection.OpenAsync(); +``` + +Подключение нужно открыть перед запросом и закрыть после. Закрывать вручную не нужно: +`await using` сделает это сам, даже если внутри случилась ошибка. + +Создаёт подключения класс `Data/DbConnectionFactory.cs`. Он читает строку подключения +из `appsettings.json`, поэтому в коде нет ни логинов, ни паролей. + +### SqlCommand — команда + +`SqlCommand` хранит сам запрос: текст SQL или имя хранимой процедуры. + +```csharp +await using var command = new SqlCommand("SELECT Id, Name FROM Products", connection); +``` + +Выполнить команду можно тремя способами: + +| Способ | Что делает | Когда нужен | +|---|---|---| +| `ExecuteReaderAsync()` | возвращает строки результата | когда читаем `SELECT` | +| `ExecuteScalarAsync()` | возвращает одно значение | когда нужно одно число, например новый `Id` | +| `ExecuteNonQueryAsync()` | возвращает число изменённых строк | для `INSERT`, `UPDATE`, `DELETE` | + +### SqlParameter — параметры и защита от SQL Injection + +Пользовательский ввод нельзя вставлять в текст запроса склейкой строк. + +Так делать нельзя: + +```csharp +// Неправильно +string query = "SELECT * FROM Products WHERE Name = '" + name + "'"; +``` + +Если пользователь введёт `' OR '1'='1`, запрос станет +`WHERE Name = '' OR '1'='1'` и вернёт все товары. Ещё хуже — можно подставить +`'; DROP TABLE Products; --` и удалить таблицу. Это и называется SQL Injection. + +Правильно — передавать значения через параметры: + +```csharp +// Правильно +command.Parameters.Add("@Id", SqlDbType.Int).Value = id; +``` + +Значение уходит на сервер отдельно от текста запроса, поэтому подставить свой SQL +через него невозможно. `SqlDbType.Int` указывает тип заранее: так сервер понимает +данные быстрее и правильнее, чем при `AddWithValue`. + +Во всём проекте текст запроса — это неизменяемая строка, а вставляются только +параметры. Ни одного такого склеивания в коде нет. + +### SqlDataReader — чтение строк + +`SqlDataReader` возвращает строки по одной. Он работает только вперёд, поэтому +подходит для больших таблиц: память не переполняется. + +```csharp +// Выполняем SELECT и читаем результат построчно +await using var reader = await command.ExecuteReaderAsync(); + +while (await reader.ReadAsync()) +{ + string name = reader.GetString(reader.GetOrdinal("Name")); + decimal price = reader.GetDecimal(reader.GetOrdinal("Price")); + + // Если поле может быть пустым, проверяем это + string? description = reader.IsDBNull(reader.GetOrdinal("Description")) + ? null + : reader.GetString(reader.GetOrdinal("Description")); +} +``` + +`ReadAsync()` возвращает `true`, пока есть строки. Пока открыт `reader`, другие +команды на этом же подключении выполнять нельзя. + +### SqlDataAdapter и DataTable — работа без базы + +Иногда удобно забрать все данные в память и закрыть подключение. Это делает +`SqlDataAdapter`: + +```csharp +await using var connection = _connectionFactory.CreateConnection(); +await using var command = new SqlCommand("SELECT Id, Name, Price FROM Products", connection); + +using var adapter = new SqlDataAdapter(command); +var table = new DataTable(); + +// Сам открывает подключение, забирает данные и закрывает подключение +await Task.Run(() => adapter.Fill(table)); +``` + +`Fill()` — синхронный метод, поэтому он обёрнут в `Task.Run`. + +Дальше с таблицей можно работать как с обычной таблицей в памяти. База при этом +свободна и не ждёт: + +```csharp +foreach (DataRow row in table.Rows) +{ + int id = (int)row["Id"]; + string name = (string)row["Name"]; + decimal price = (decimal)row["Price"]; + Console.WriteLine($"{id} {name} {price:C}"); +} +``` + +### SqlTransaction — всё или ничего + +Транзакция — это несколько запросов, которые выполняются вместе. Если хотя бы один +провалился, отменяются все. + +Создание заказа как раз такой случай. Заказ состоит из нескольких действий: + +``` +создать заказ +для каждого товара: + уменьшить остаток на складе + добавить позицию в заказ +``` + +Если второго товара не оказалось на складе, нельзя оставлять базу в таком виде: +заказ есть, а позиций нет. Транзакция отменяет всё целиком. + +```csharp +await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync(); + try { - switch (input) { /* ... */ } + // создаём заказ + // списываем остатки и добавляем позиции + + await transaction.CommitAsync(); // всё прошло успешно } catch (Exception ex) +{ + await transaction.RollbackAsync(); // отменяем все изменения + throw; +} +``` + +Важная деталь: каждая команда внутри транзакции получает третий параметр — +саму транзакцию. Если его забыть, сервер вернёт ошибку. + +```csharp +await using var command = new SqlCommand(query, connection, transaction); +``` + +## Connected и Disconnected — разница + +| | Connected | Disconnected | +|---|---|---| +| Классы | `SqlConnection`, `SqlCommand`, `SqlDataReader` | `SqlDataAdapter`, `DataTable` | +| Подключение | открыто всё время чтения | открыто только на время загрузки | +| Чтение | по одной строке | сразу вся таблица | +| Память | тратится мало | тратится на всю выборку | +| Когда применять | много данных | мало данных и нужна удобная работа с ними | + +Простое правило: данных много — берём `SqlDataReader`, данных мало и нужно +покрутить их в памяти — берём `SqlDataAdapter`. + +## Структура проекта + +``` +Artem/ +├── README.md +├── AdoNetShop.slnx файл решения +│ +├── AdoNetShop/ сам проект +│ ├── Program.cs консольное меню +│ ├── appsettings.json строка подключения +│ ├── AdoNetShop.csproj файл проекта +│ │ +│ ├── Models/ данные магазина, без логики +│ │ ├── Product.cs +│ │ ├── Category.cs +│ │ ├── Customer.cs +│ │ ├── Order.cs +│ │ └── OrderItem.cs +│ │ +│ ├── Data/ вся работа с базой +│ │ ├── DbConnectionFactory.cs создаёт SqlConnection +│ │ ├── ProductRepository.cs CRUD товаров, DataAdapter, процедуры +│ │ ├── CategoryRepository.cs чтение категорий +│ │ ├── CustomerRepository.cs чтение клиентов +│ │ └── OrderRepository.cs заказ в транзакции +│ │ +│ └── Services/ +│ └── ShopService.cs простые проверки перед сохранением +│ +└── database/ скрипты для SQL Server + ├── 01_create_database.sql + ├── 02_create_tables.sql + ├── 03_insert_data.sql + ├── 04_queries.sql + ├── 05_procedures.sql + └── 06_test.sql +``` + +Разделение простое: `Models` — что есть в магазине, `Data` — как это лежит в базе, +`Services` — что с этим можно сделать, `Program` — как это показываем пользователю. + +## Таблицы в базе + +| Таблица | Что хранит | +|---|---| +| `Categories` | категории товаров: ноутбуки, смартфоны, аксессуары | +| `Products` | товары: название, описание, цена, остаток, категория | +| `Customers` | клиенты: имя, email, телефон | +| `Orders` | заказы: кто, когда, статус, сумма | +| `OrderItems` | что именно заказали: товар, количество, цена | + +Что настроено в таблицах и зачем: + +- `IDENTITY` — база сама выдаёт `Id`. +- `PRIMARY KEY` — строки не повторяются. +- `FOREIGN KEY` — нельзя создать заказ для несуществующего клиента. +- `CHECK` — цена больше нуля, количество в заказе больше нуля. +- `UNIQUE` — нельзя завести двух клиентов с одним email. +- `INDEX` на внешних ключах — быстрее работают `JOIN`. +- `DECIMAL(18,2)` для денег — не `FLOAT`, потому что он даёт ошибки округления. +- `NVARCHAR` вместо `VARCHAR` — чтобы работала кириллица. + +## Что умеет программа + +В меню 12 пунктов. Каждый показывает свой элемент ADO.NET. + +| Пункт | Что делает | Что показывает | +|---|---|---| +| 1 | Список товаров | `SqlDataReader`, `ExecuteReaderAsync` | +| 2 | Найти товар по номеру | `SqlParameter`, параметризованный запрос | +| 3 | Добавить товар | `ExecuteScalarAsync`, получение нового `Id` | +| 4 | Изменить товар | `ExecuteNonQueryAsync` для `UPDATE` | +| 5 | Удалить товар | `ExecuteNonQueryAsync` для `DELETE` | +| 6 | Список категорий | простой `SELECT` | +| 7 | Список клиентов | простой `SELECT` | +| 8 | Создать заказ | транзакция: `Commit` или `Rollback` | +| 9 | Список заказов | чтение результата запроса | +| 10 | DataAdapter | `SqlDataAdapter` и `DataTable` | +| 11 | Stored Procedures | `CommandType.StoredProcedure`, ошибка `RAISERROR` | +| 12 | Откат транзакции | наглядный `Rollback` | + +### Пункт 8. Заказ и транзакция + +Программа спрашивает клиента и список товаров, затем в одной транзакции создаёт заказ, +списывает остатки и добавляет позиции. Если товара не хватает, база остаётся +без изменений: заказ не появляется, остатки прежние. + +### Пункт 12. Наглядный откат + +Этот пункт специально для защиты. Метод `CreateOrderWithRollbackDemoAsync()` +делает так: + +1. Открывает транзакцию. +2. Создаёт заказ. +3. Успешно списывает две штуки первого товара. +4. Падает с ошибкой. +5. Выполняет `Rollback`. +6. Показывает остатки заново. + +В консоли видно: списание прошло успешно, но после отката остатки такие же, +как были до операции. Это и доказывает главное свойство транзакции. + +### Пункт 11. Хранимые процедуры + +Вызываются процедуры `dbo.GetProducts`, `dbo.GetProductById`, `dbo.UpdateProductPrice`. +Если ввести цену `-5`, процедура вернёт ошибку через `RAISERROR`, а программа +покажет её как `SqlException`. + +Бизнес-логика остаётся в приложении. Процедуры здесь только для демонстрации, +что ADO.NET умеет их вызывать. + +## Проверка ввода и ошибок + +Перед сохранением проверяется: + +- номер введён как число; +- цена больше нуля; +- остаток не отрицательный; +- количество в заказе больше нуля; +- товар существует и его хватает; +- email не пустой. + +Ошибки не скрываются: в консоль выводится сообщение, а для SQL Server показывается +код ошибки. В `Program.cs` главный цикл оборачивает каждый пункт в `try`/`catch`: + +```csharp +catch (Exception ex) { Console.WriteLine($"[ОШИБКА]: {ex.Message}"); if (ex.InnerException != null) @@ -931,108 +410,63 @@ catch (Exception ex) } ``` -Такой подход выбран сознательно: **для учебного проекта исключения не скрываются -молча** — пользователь видит, что произошло. В продакшене ошибки логировали бы -в файл, а пользователю показывали бы нейтральное сообщение. +Для учебной работы такой подход удобен: видно, что произошло. В настоящем проекте +подробности писали бы в лог, а пользователю показывали короткое сообщение. -Дополнительно ошибки от самой СУБД обрабатываются отдельно. `SqlException` — -это исключение SQL Server, в нём есть номер ошибки и текст от сервера: +## Что показать преподавателю -```csharp -catch (SqlException ex) -{ - Console.WriteLine($"Ошибка SQL Server (код {ex.Number}): {ex.Message}"); - return false; -} +1. `Data/DbConnectionFactory.cs` — подключение и строка из конфига. +2. `Data/ProductRepository.cs` — три способа выполнить команду, параметры, + `SqlDataReader`, `SqlDataAdapter`, вызов процедуры. +3. `Data/OrderRepository.cs` — транзакция: успешный заказ и откат. +4. Пункты меню 8, 10, 11, 12 — прямо во время защиты. + +## Если что-то не работает + +| Сообщение | Причина | Что делать | +|---|---|---| +| Сервер не найден или недоступен | SQL Server не запущен | Запустите службу SQL Server | +| Не удалось открыть соединение | неверное имя экземпляра | для Express замените `Server` на `.\\SQLEXPRESS` | +| Login failed for user | неверный способ входа | проверьте `Trusted_Connection=True` | +| Недоверенный сертификат | нет SSL-сертификата | добавьте `TrustServerCertificate=True` | +| Cannot open database "AdoNetShop" | база не создана | выполните `01_create_database.sql` | +| Invalid object name 'Products' | таблиц нет | выполните `02_create_tables.sql` | +| Could not find file appsettings.json | запустили не из той папки | запускайте из папки `AdoNetShop` | +| Требуется .NET, которого нет | нет runtime .NET 8 | установите .NET 8 SDK | +| Повторный запуск `03_insert_data.sql` даёт ошибку FK | данные вставлялись вручную | запустите скрипт заново, он чистит таблицы сам | + +## Как менять строку подключения + +Строка лежит в `AdoNetShop/appsettings.json`. Разбор по параметрам: + +| Параметр | Значение | Зачем | +|---|---|---| +| `Server` | `localhost` | адрес сервера | +| `Database` | `AdoNetShop` | имя базы | +| `Trusted_Connection` | `True` | вход через Windows, без логина и пароля | +| `TrustServerCertificate` | `True` | доверять сертификату локального сервера | + +Если нужен вход по логину и паролю: + +``` +Server=localhost;Database=AdoNetShop;User Id=sa;Password=Пароль;TrustServerCertificate=True; ``` ---- +Пароль в файле конфигурации — это вариант для учёбы. В настоящем проекте такие +данные хранят в переменных окружения. Важно, что в коде пароля нет. -## 13. Какие файлы нужно изучить в первую очередь +## Замечание про кодировку -Если преподаватель спросит «покажи, где у тебя ADO.NET», открывайте эти файлы -в таком порядке: +SQL-файлы сохранены в UTF-8 с меткой в начале файла, поэтому кириллица правильно +читается в SSMS, Visual Studio и в командной строке: -### 1. `AdoNetShop/Data/DbConnectionFactory.cs` — подключение -Класс, который создаёт `SqlConnection`. Здесь видно, что строка подключения -берётся из `appsettings.json`. +```bash +sqlcmd -S localhost -E -f 65001 -i database\01_create_database.sql +``` -### 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`. -* `CreateOrderWithRollbackDemoAsync()` — наглядная демонстрация отката - для защиты: часть операций успевает выполниться, потом происходит ошибка, - и `RollbackAsync()` возвращает базу в исходное состояние. - -### 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` | -| `You must install or update .NET to run this application` | На компьютере нет runtime .NET 8 | Установите .NET 8 SDK **или** проверьте, что в `.csproj` есть `LatestMajor` | -| Вместо русских букв «кракозябры» | Скрипт сохранён не в UTF-8 | Не пересохраняйте `.sql` в ANSI-кодировке либо запускайте `sqlcmd -f 65001 -i файл.sql` | -| Ошибка FOREIGN KEY при повторном запуске `03_insert_data.sql` | Ручная вставка данных без `IDENTITY_INSERT` | Запустите скрипт заново: он сам удаляет старые данные и подставляет свои Id | - ---- - -## Краткая памятка: 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` | -| 13 | `Commit` / `Rollback` | `CreateOrderWithTransactionAsync` | -| 14 | Наглядный откат (отмена выполненных операций) | `CreateOrderWithRollbackDemoAsync` | - ---- - -*Проект создан в учебных целях для лабораторной работы по технологии ADO.NET. -ADO.NET используется как основной и единственный способ работы с базой данных.* \ No newline at end of file +Проект собран под `net8.0`. Если на компьютере стоит только .NET 9, 10 или новее, +приложение всё равно запустится: в `.csproj` включён `RollForward`. Если понадобится, +строку `net8.0` можно поменять на установленную версию. \ No newline at end of file