Перейти к основному содержимому

ADO.NET и Dapper — первая программа

Разработчику
Загрузка симулятора первой программы…

ADO.NET и Dapper — первая программа

Где применяют прямой SQL

В EF Core вы описываете классы, а фреймворк строит SQL. Иногда нужен полный контроль над текстом запроса: сложные отчёты, оконные функции SQL, массовая загрузка, legacy-база со странной схемой, микросервис «только чтение» с жёсткими требованиями к производительности.

Для этого в .NET есть два уровня:

УровеньЧто это
ADO.NETНизкоуровневый API: открыть соединение, выполнить команду, прочитать строки по одной.
DapperТонкая обёртка над ADO.NET: вы по-прежнему пишете SQL, но результат сразу маппится в объекты C#.

Обзор всех способов: 44. Высокоуровневый путь: EF Core — первая программа. Историческая модель COM-ADO: ADO.NET (платформа .NET).


Словарь

ТерминПростыми словами
Соединение (connection)Канал к файлу БД или серверу SQL; открывается перед запросом, закрывается после.
Команда (command)Текст SQL + параметры; выполняется через ExecuteNonQuery (без строк) или ExecuteReader (с выборкой).
ПараметрЗначение @title, @year — подставляется драйвером безопасно, отдельно от текста SQL.
DataReaderПоток строк результата; читается в цикле ReadAsync().
Строка подключенияСтрока вида Data Source=books_ado.db — адрес и опции БД.
МаппингПревращение колонок SQL в свойства класса Book.

Что получится

ЭтапТехнологияДействие
1ADO.NET + Microsoft.Data.SqliteСоздать таблицу, INSERT с параметрами, SELECT в цикле
2DapperТе же операции короче: ExecuteAsync, QueryAsync<Book>

Требования

  • .NET SDK 8+
  • Базовый SQL (что такое SELECT, INSERT, WHERE): Основы БД

Проект

dotnet new console -n BooksAdo -o BooksAdo
cd BooksAdo
dotnet add package Microsoft.Data.Sqlite
dotnet add package Dapper

Microsoft.Data.Sqlite — современный драйвер SQLite от Microsoft (для SQL Server используют Microsoft.Data.SqlClient — другой пакет, другая строка подключения).


Модель

Models/Book.cs:

namespace BooksAdo.Models;

public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int Year { get; set; }
}

Тот же класс, что в 441: три поля соответствуют трём колонкам таблицы. Dapper сопоставит их по имени (регистр не важен: Titletitle).


ADO.NET — создать таблицу и записать данные

Program.cs (фрагмент):

using Microsoft.Data.Sqlite;
using BooksAdo.Models;

const string cs = "Data Source=books_ado.db";

await using (var conn = new SqliteConnection(cs))
{
await conn.OpenAsync();

var create = """
CREATE TABLE IF NOT EXISTS Books (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Title TEXT NOT NULL,
Year INTEGER NOT NULL
);
""";
await using (var cmd = new SqliteCommand(create, conn))
await cmd.ExecuteNonQueryAsync();

const string insert = "INSERT INTO Books (Title, Year) VALUES (@title, @year);";
await using var insertCmd = new SqliteCommand(insert, conn);
insertCmd.Parameters.AddWithValue("@title", "Clean Code");
insertCmd.Parameters.AddWithValue("@year", 2008);
await insertCmd.ExecuteNonQueryAsync();
}

Построчный разбор

СтрокаСмысл
const string cs = "Data Source=books_ado.db"Строка подключения: файл БД в текущей папке.
new SqliteConnection(cs)Объект соединения (ещё закрыт).
await conn.OpenAsync()Открыть канал к файлу; без этого команды не выполнятся.
""" ... """Raw string literal в C# — многострочный SQL без экранирования кавычек.
CREATE TABLE IF NOT EXISTSСоздать таблицу, если её ещё нет (повторный запуск безопасен).
AUTOINCREMENTSQLite сам увеличивает Id для новых строк.
ExecuteNonQueryAsync()Выполнить SQL без возврата таблицы (CREATE, INSERT, UPDATE, DELETE).
@title, @year в SQLИменованные параметры — плейсхолдеры для значений.
Parameters.AddWithValue("@title", "Clean Code")Подставить значение в параметр; драйвер экранирует спецсимволы.

Почему параметры обязательны

Если склеивать SQL из строк пользователя ($"INSERT ... '{userInput}'"), злоумышленник может вставить свой фрагмент SQL (SQL-инъекция). С @параметрами текст запроса и данные разделены: СУБД получает готовый шаблон и значения отдельно.

await using

Автоматически вызывает Dispose у соединения и команд — освобождает файл и сокеты. В ASP.NET соединение держат коротко: открыли → один-два запроса → закрыли; пул соединений на сервере переиспользует физические подключения.


ADO.NET — чтение DbDataReader

await using var conn = new SqliteConnection(cs);
await conn.OpenAsync();

await using var cmd = new SqliteCommand(
"SELECT Id, Title, Year FROM Books WHERE Year >= @y ORDER BY Title;",
conn);
cmd.Parameters.AddWithValue("@y", 2000);

await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var id = reader.GetInt32(0);
var title = reader.GetString(1);
var year = reader.GetInt32(2);
Console.WriteLine($"{id}: {title} ({year})");
}
ШагОбъяснение
ExecuteReaderAsync()Запрос с возвратом набора строк (курсор).
ReadAsync()Перейти к следующей строке; false — строк больше нет.
GetInt32(0), GetString(1)Взять значение колонки по индексу (0 — первая колонка в SELECT).

Индексы хрупки: если в SELECT поменять порядок колонок, код сломается. В продакшене чаще:

var titleOrdinal = reader.GetOrdinal("Title");
var title = reader.GetString(titleOrdinal);

Dapper — тот же сценарий короче

После создания таблицы (как выше):

await using var conn = new SqliteConnection(cs);
await conn.OpenAsync();

await conn.ExecuteAsync(
"INSERT INTO Books (Title, Year) VALUES (@Title, @Year);",
new { Title = "CLR via C#", Year = 2012 });

var books = await conn.QueryAsync<Book>(
"SELECT Id, Title, Year FROM Books WHERE Year >= @minYear ORDER BY Title;",
new { minYear = 2010 });

foreach (var b in books)
Console.WriteLine($"{b.Id}: {b.Title}");
ВызовЧто делает
ExecuteAsync(sql, obj)Выполняет команду без чтения строк; свойства анонимного объекта new { Title = ..., Year = ... } становятся параметрами @Title, @Year.
QueryAsync<Book>(sql, obj)Выполняет SELECT и для каждой строки создаёт Book, заполняя свойства по именам колонок.

Имена в SQL (@minYear) и в объекте (minYear) должны совпадать (без @ в свойстве). Если колонка называется book_title, в SQL можно alias: SELECT book_title AS Title.

Dapper не отслеживает изменения: чтобы обновить книгу, пишете отдельный UPDATE (или переходите на EF Core).


Транзакция (два INSERT «всё или ничего»)

Если второй INSERT упадёт, первый откатится:

await using var conn = new SqliteConnection(cs);
await conn.OpenAsync();
await using var tx = await conn.BeginTransactionAsync();

try
{
await conn.ExecuteAsync(
"INSERT INTO Books (Title, Year) VALUES (@Title, @Year);",
new { Title = "Book A", Year = 2020 },
transaction: tx);
await conn.ExecuteAsync(
"INSERT INTO Books (Title, Year) VALUES (@Title, @Year);",
new { Title = "Book B", Year = 2021 },
transaction: tx);
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}

Транзакция — группа команд с общим исходом: либо все применились, либо ни одна.


Когда что выбирать

КритерийADO.NETDapperEF Core
Скорость старта CRUDСредняяБыстраяБыстрая
Контроль SQLПолныйПолныйЧастичный (LINQ → SQL)
Миграции схемыВручную / скриптыВручнуюВстроены
Отслеживание измененийНетНетДа
Типичное применениеETL, отчёты, тонкая настройкаHigh-load read APIДоменные приложения, CRUD

Частые ошибки

ОшибкаРешение
Склейка пользовательского ввода в SQLТолько @параметры
Соединение открыто часамиКороткие using; в вебе — на время запроса
Путаница SqlClient и SqliteSQLite → Microsoft.Data.Sqlite; SQL Server → Microsoft.Data.SqlClient
Dapper: поле null / не маппитсяИмя колонки = имя свойства или AS Alias в SQL
«database is locked»Закрыть reader и connection; в тестах — не держать два writer’а на один файл

Что попробовать

  1. UPDATE Books SET Year = @year WHERE Id = @id и DELETE FROM Books WHERE Id = @id с параметрами.
  2. Прочитайте список через ADO.NET, тот же список — через Dapper; сравните строки кода.
  3. Перепишите сценарий на EF Core и сравните, где появляются миграции и SaveChanges.

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).