# AdoNetShop — лабораторная работа по ADO.NET Небольшой интернет-магазин на 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. Создайте базу данных.** Откройте SSMS, подключитесь к своему серверу и выполните файлы из папки `database` строго по порядку: | Файл | Что делает | |---|---| | `01_create_database.sql` | создаёт базу `AdoNetShop` | | `02_create_tables.sql` | создаёт таблицы, ключи, ограничения, индексы | | `03_insert_data.sql` | добавляет тестовые данные | | `05_procedures.sql` | создаёт хранимые процедуры | Файлы `04_queries.sql` и `06_test.sql` — это примеры запросов для проверки. Их можно не выполнять. Все скрипты можно запускать повторно: старые данные удаляются, новые добавляются с теми же номерами. **Шаг 2. Проверьте строку подключения.** Файл `AdoNetShop/appsettings.json`: ```json { "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=AdoNetShop;Trusted_Connection=True;TrustServerCertificate=True;" } } ``` Если сервер называется иначе, поменяйте только параметр `Server`: ``` Server=localhost обычный SQL Server Server=.\\SQLEXPRESS SQL Server Express Server=(localdb)\\MSSQLLocalDB LocalDB ``` В JSON обратный слэш пишется дважды: `\\` вместо `\`. Подробнее про каждый параметр строки подключения — в разделе ниже. **Шаг 3. Запустите приложение.** ```bash cd AdoNetShop dotnet run ``` **Шаг 4. Пользуйтесь меню.** Если база создана неправильно или сервер выключен, программа не сломается: она покажет понятное сообщение об ошибке и продолжит работать. ## Что такое ADO.NET ADO.NET — это набор классов, через которые программа на C# общается с базой данных. Без него приложение не знает, как отправить запрос в SQL Server и как получить ответ. Схема выглядит так: ``` Приложение на C# <--> ADO.NET <--> SQL Server наш код переводчик где лежат данные ``` ### Зачем он нужен - Можно писать любой SQL, какой поддерживает база. ORM так не умеет без обходных путей. - Работает быстро: нет лишних проверок и отслеживания изменений, которые делают ORM. - Показывает, как устроена работа с базой на самом деле. Любой ORM внутри делает почти то же самое, только прячет это от вас. ### Что такое провайдер ADO.NET сам не ходит в базу. Для каждой СУБД есть отдельная библиотека — это и есть провайдер: | База | Провайдер | |---|---| | SQL Server | `Microsoft.Data.SqlClient` | | PostgreSQL | `Npgsql` | | MySQL | `MySqlConnector` | | SQLite | `Microsoft.Data.Sqlite` | В нашем проекте используется `Microsoft.Data.SqlClient`. Он подключается одной строкой в файле проекта: ```xml ``` ## Главные классы ADO.NET ### 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 { // создаём заказ // списываем остатки и добавляем позиции 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) Console.WriteLine($"[ДЕТАЛИ]: {ex.InnerException.Message}"); } ``` Для учебной работы такой подход удобен: видно, что произошло. В настоящем проекте подробности писали бы в лог, а пользователю показывали короткое сообщение. ## Что показать преподавателю 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; ``` Пароль в файле конфигурации — это вариант для учёбы. В настоящем проекте такие данные хранят в переменных окружения. Важно, что в коде пароля нет. ## Замечание про кодировку SQL-файлы сохранены в UTF-8 с меткой в начале файла, поэтому кириллица правильно читается в SSMS, Visual Studio и в командной строке: ```bash sqlcmd -S localhost -E -f 65001 -i database\01_create_database.sql ``` Если пересохранить файл в другой кодировке, русские названия превратятся в нечитаемый текст. Проект собран под `net8.0`. Если на компьютере стоит только .NET 9, 10 или новее, приложение всё равно запустится: в `.csproj` включён `RollForward`. Если понадобится, строку `net8.0` можно поменять на установленную версию.