472 lines
23 KiB
Markdown
472 lines
23 KiB
Markdown
# 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
|
||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
|
||
```
|
||
|
||
## Главные классы 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`. Если понадобится,
|
||
строку `<TargetFramework>net8.0</TargetFramework>` можно поменять на установленную версию. |