AdoNetShop/README.md

23 KiB
Raw Permalink Blame History

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:

{
  "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. Запустите приложение.

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. Он подключается одной строкой в файле проекта:

<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />

Главные классы ADO.NET

SqlConnection — подключение к базе

SqlConnection — это подключение к серверу. Создаётся из строки подключения.

await using var connection = new SqlConnection(connectionString);

// Открываем подключение
await connection.OpenAsync();

Подключение нужно открыть перед запросом и закрыть после. Закрывать вручную не нужно: await using сделает это сам, даже если внутри случилась ошибка.

Создаёт подключения класс Data/DbConnectionFactory.cs. Он читает строку подключения из appsettings.json, поэтому в коде нет ни логинов, ни паролей.

SqlCommand — команда

SqlCommand хранит сам запрос: текст SQL или имя хранимой процедуры.

await using var command = new SqlCommand("SELECT Id, Name FROM Products", connection);

Выполнить команду можно тремя способами:

Способ Что делает Когда нужен
ExecuteReaderAsync() возвращает строки результата когда читаем SELECT
ExecuteScalarAsync() возвращает одно значение когда нужно одно число, например новый Id
ExecuteNonQueryAsync() возвращает число изменённых строк для INSERT, UPDATE, DELETE

SqlParameter — параметры и защита от SQL Injection

Пользовательский ввод нельзя вставлять в текст запроса склейкой строк.

Так делать нельзя:

// Неправильно
string query = "SELECT * FROM Products WHERE Name = '" + name + "'";

Если пользователь введёт ' OR '1'='1, запрос станет WHERE Name = '' OR '1'='1' и вернёт все товары. Ещё хуже — можно подставить '; DROP TABLE Products; -- и удалить таблицу. Это и называется SQL Injection.

Правильно — передавать значения через параметры:

// Правильно
command.Parameters.Add("@Id", SqlDbType.Int).Value = id;

Значение уходит на сервер отдельно от текста запроса, поэтому подставить свой SQL через него невозможно. SqlDbType.Int указывает тип заранее: так сервер понимает данные быстрее и правильнее, чем при AddWithValue.

Во всём проекте текст запроса — это неизменяемая строка, а вставляются только параметры. Ни одного такого склеивания в коде нет.

SqlDataReader — чтение строк

SqlDataReader возвращает строки по одной. Он работает только вперёд, поэтому подходит для больших таблиц: память не переполняется.

// Выполняем 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:

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.

Дальше с таблицей можно работать как с обычной таблицей в памяти. База при этом свободна и не ждёт:

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 — всё или ничего

Транзакция — это несколько запросов, которые выполняются вместе. Если хотя бы один провалился, отменяются все.

Создание заказа как раз такой случай. Заказ состоит из нескольких действий:

создать заказ
для каждого товара:
    уменьшить остаток на складе
    добавить позицию в заказ

Если второго товара не оказалось на складе, нельзя оставлять базу в таком виде: заказ есть, а позиций нет. Транзакция отменяет всё целиком.

await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();

try
{
    // создаём заказ
    // списываем остатки и добавляем позиции

    await transaction.CommitAsync();   // всё прошло успешно
}
catch (Exception ex)
{
    await transaction.RollbackAsync(); // отменяем все изменения
    throw;
}

Важная деталь: каждая команда внутри транзакции получает третий параметр — саму транзакцию. Если его забыть, сервер вернёт ошибку.

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:

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 и в командной строке:

sqlcmd -S localhost -E -f 65001 -i database\01_create_database.sql

Если пересохранить файл в другой кодировке, русские названия превратятся в нечитаемый текст.

Проект собран под net8.0. Если на компьютере стоит только .NET 9, 10 или новее, приложение всё равно запустится: в .csproj включён RollForward. Если понадобится, строку <TargetFramework>net8.0</TargetFramework> можно поменять на установленную версию.