Уявіть, що ви створили чудовий консольний додаток для управління бібліотекою. Усі книги зберігаються у List<Book>, пошук працює миттєво, сортування — бездоганне. Ви задоволені. Але наступного дня ви перезапускаєте програму — і список порожній. Усі дані зникли. Чому? Тому що List<T> живе виключно в оперативній пам'яті (RAM), і коли процес завершується, пам'ять звільняється.
«Ну добре, — скажете ви, — тоді збережемо все у файл!» І це правильна думка. Ви вже знаєте, як працювати з файлами через System.IO. Але уявіть, що ваша бібліотечна система масштабується: 50 000 книг, 10 000 читачів, 200 одночасних користувачів. Збереження всього у JSON-файл стає кошмаром:
Саме для вирішення цих проблем і існують бази даних (databases) — спеціалізоване програмне забезпечення, оптимізоване для зберігання, пошуку та маніпулювання структурованими даними. Але між вашим C#-додатком і базою даних існує «прірва» — вони говорять різними мовами. Ваш код — це об'єкти, класи, методи. База даних — це таблиці, рядки, SQL-запити.
ADO.NET — це міст через цю прірву. Це набір класів у .NET, який дозволяє вашому C#-коду «спілкуватися» з базами даних: підключатися, надсилати SQL-запити, отримувати результати та працювати з ними як зі звичайними об'єктами C#.
using-інструкцій, а також базове розуміння SQL та реляційних баз даних. Рекомендується мати встановлений MS SQL Server (або SQL Server Express).Щоб зрозуміти, чому ADO.NET виглядає саме так, а не інакше, варто простежити еволюцію технологій доступу до даних у світі Microsoft. Кожна нова технологія з'являлася як відповідь на обмеження попередньої.
Розглянемо кожну технологію детальніше, щоб зрозуміти, які проблеми вони вирішували і чому ADO.NET став таким, яким ми його знаємо.
ODBC — це перша спроба створити стандартний інтерфейс для доступу до різних баз даних. До ODBC кожна СУБД мала свій власний, унікальний API. Якщо ви писали програму для Oracle, а потім клієнт захотів перейти на SQL Server, вам потрібно було переписувати весь код доступу до даних з нуля.
ODBC вирішив цю проблему через концепцію драйвера — спеціальної бібліотеки, яка «перекладає» стандартні виклики ODBC у команди, зрозумілі конкретній СУБД. Ваш додаток «говорить» з ODBC Manager, а ODBC Manager через відповідний драйвер — з базою даних.
Аналогія: ODBC — це як перекладач-синхроніст на міжнародній конференції. Ви говорите англійською (стандартний API), перекладач перекладає на японську (мова конкретної СУБД).
Проблема ODBC: Низькорівневий C-based API, складний у використанні, обмежений лише реляційними базами даних.
DAO з'явився як нативний спосіб доступу до баз даних Microsoft Access (двигун Jet). Замість того щоб працювати через «перекладача» ODBC, DAO звертався до бази даних напряму, що робило його значно швидшим для Access.
Проблема DAO: Працював ефективно лише з Access/Jet. Для інших СУБД доводилося використовувати ODBC-міст, що зводило нанівець усі переваги в продуктивності.
OLE DB (Object Linking and Embedding, Database) — амбіційна спроба Microsoft створити універсальний інтерфейс для доступу не тільки до реляційних баз даних, але й до будь-яких джерел даних: електронних таблиць, поштових систем, файлових систем. OLE DB базувався на технології COM (Component Object Model).
Ідея була геніальна: один API для доступу до SQL Server, Oracle, Excel, Exchange Server та будь-чого іншого. Але реалізація виявилася надзвичайно складною — COM-інтерфейси OLE DB були заплутаними і важкими для програмістів.
ADO — це «дружня обгортка» над OLE DB. Microsoft зрозумів, що OLE DB занадто складний для більшості розробників, і створив простіший API з ключовими об'єктами: Connection, Command, RecordSet.
Саме в ADO Classic з'явилася концепція RecordSet — об'єкта, який міг завантажити дані з бази, від'єднатися від неї, дозволити клієнту працювати з даними локально, а потім синхронізувати зміни назад. Ця ідея стала основою для від'єднаного режиму (disconnected mode) в ADO.NET.
Проблема ADO Classic: Тісна прив'язка до COM, складність серіалізації для веб-додатків, RecordSet як «монолітний» об'єкт без гнучкості.
З появою .NET Framework Microsoft вирішив не просто «портувати» ADO в .NET, а повністю переосмислити архітектуру доступу до даних. Ось ключові зміни:
RecordSet з'явилися окремі класи — DataSet, DataTable, DataReader, DataAdapter — кожен з чітко визначеною роллю.DataSet легко серіалізується в XML, що ідеально підходило для веб-сервісів ери SOAP.Перш ніж рухатися далі, розберімося з двома ключовими термінами, які часто плутають: драйвер і провайдер даних (Data Provider).
Драйвер (Driver) — це низькорівневий компонент, зазвичай написаний на C/C++, який «знає», як фізично передавати байти до конкретної СУБД по мережі. Драйвер працює на рівні мережевих протоколів та бінарних форматів. Наприклад, ODBC-драйвер для SQL Server знає специфіку протоколу TDS (Tabular Data Stream).
Провайдер даних .NET (Data Provider) — це набір .NET-класів, які реалізують стандартні інтерфейси ADO.NET (IDbConnection, IDbCommand, IDataReader тощо) для конкретної СУБД. Провайдер може всередині використовувати драйвер або мати власну реалізацію мережевого протоколу.
| Характеристика | Драйвер (ODBC/OLE DB) | Провайдер даних (.NET) |
|---|---|---|
| Технологія | Нативний C/C++ код | Managed .NET код |
| Інтерфейс | ODBC API або COM-інтерфейси | .NET-інтерфейси (IDbConnection тощо) |
| Рівень абстракції | Низький — передача байтів | Високий — об'єктна модель |
| Продуктивність | Додатковий маршалінг COM↔.NET | Нативна інтеграція з .NET |
| Приклад | SQL Server ODBC Driver | Microsoft.Data.SqlClient |
ADO.NET (ActiveX Data Objects for .NET) — це набір класів у .NET для доступу до джерел даних, переважно реляційних баз даних. Незважаючи на назву, ADO.NET не має майже нічого спільного зі старим ADO Classic — це повністю нова архітектура, яка лише успадкувала назву з маркетингових міркувань.
Формально ADO.NET — це частина Base Class Library (BCL) .NET, розташована у просторі імен System.Data та його підпросторах. ADO.NET надає:
🔌 Підключення
📤 Виконання команд
📥 Читання даних
🔄 Офлайн-робота
🔒 Безпека
⚡ Асинхронність
Може виникнути питання: «Чи не застарів ADO.NET? Усі використовують Entity Framework або Dapper!» Це поширена помилка. ADO.NET — це фундамент, на якому побудовані всі інші технології доступу до даних у .NET:
DbConnection, DbCommand та DbDataReader з ADO.NET.IDbConnection з ADO.NET.Розуміння ADO.NET дає вам повний контроль над взаємодією з базою даних та допомагає зрозуміти, що відбувається «під капотом» ORM.
Зверніть увагу на архітектуру: ADO.NET займає центральну позицію між вашим кодом та базою даних. Entity Framework, Dapper та інші ORM — це лише зручніші «обгортки» над ADO.NET, які автоматизують рутинні операції (маппінг, генерація SQL тощо), але зрештою делегують усю роботу саме ADO.NET.
ADO.NET пропонує дві принципово різні моделі роботи з даними: присоединённий режим (Connected Mode) та від'єднаний режим (Disconnected Mode). Розуміння різниці між ними — ключ до правильного вибору підходу для конкретної задачі.
У цьому режимі ваш додаток утримує відкрите з'єднання з базою даних протягом усього часу роботи з даними. Ви надсилаєте SQL-запит, отримуєте результати через DataReader і обробляєте їх «на льоту» — рядок за рядком. Після завершення обробки з'єднання закривається.
Аналогія: Присоединённый режим — це як телефонний дзвінок. Ви набираєте номер (відкриваєте з'єднання), ведете розмову (читаєте дані), кладете трубку (закриваєте з'єднання). Поки ви на лінії, лінія зайнята — ніхто інший не може використовувати це з'єднання.
Ключові класи: DbConnection, DbCommand, DbDataReader.
Переваги:
Недоліки:
У цьому режимі ваш додаток завантажує дані в пам'ять у вигляді DataSet або DataTable, після чого закриває з'єднання. Усі маніпуляції з даними (фільтрація, сортування, зміни) відбуваються локально. Коли потрібно зберегти зміни, з'єднання відкривається знову, зміни синхронізуються з базою, з'єднання закривається.
Аналогія: Від'єднаний режим — це як бібліотека. Ви приходите (відкриваєте з'єднання), берете книжку (завантажуєте дані), йдете додому (закриваєте з'єднання), читаєте вдома (працюєте з DataSet), повертаєте книжку (синхронізуєте зміни).
Ключові класи: DataSet, DataTable, DataAdapter, DataRow, DataColumn.
Переваги:
Недоліки:
| Критерій | Connected Mode | Disconnected Mode |
|---|---|---|
| Час з'єднання | Протягом усієї операції | Лише для fetch/update |
| Пам'ять | Мінімальна (один рядок) | Усі дані в RAM |
| Актуальність | Завжди актуальні | Можуть застаріти |
| Масштабованість | Обмежена кількістю з'єднань | Висока |
| Офлайн-робота | Неможлива | Підтримується |
| Типовий клас | DbDataReader | DataSet / DataTable |
| Коли використовувати | Читання великих обсягів, потокова обробка | CRUD-інтерфейси, робота з формами |
ADO.NET організований у кілька просторів імен (namespaces), кожен з яких відповідає за окрему частину функціональності. Розуміння цієї структури допоможе вам знайти потрібний клас і зрозуміти його роль.
System.Data.SqlClient — це застарілий провайдер для SQL Server, який більше не отримує нових функцій. Для нових проєктів завжди використовуйте Microsoft.Data.SqlClient, який встановлюється через NuGet. Він підтримує найновіші можливості SQL Server, включаючи Always Encrypted, Azure AD аутентифікацію тощо.Окрім стандартних провайдерів, існують провайдери від сторонніх розробників для різних СУБД. Усі вони реалізують ті самі абстрактні класи з System.Data.Common, тому ваші знання ADO.NET працюватимуть з будь-якою базою даних:
| СУБД | NuGet-пакет | Простір імен | Класи |
|---|---|---|---|
| MS SQL Server | Microsoft.Data.SqlClient | Microsoft.Data.SqlClient | SqlConnection, SqlCommand |
| PostgreSQL | Npgsql | Npgsql | NpgsqlConnection, NpgsqlCommand |
| MySQL | MySqlConnector | MySqlConnector | MySqlConnection, MySqlCommand |
| SQLite | Microsoft.Data.Sqlite | Microsoft.Data.Sqlite | SqliteConnection, SqliteCommand |
| Oracle | Oracle.ManagedDataAccess.Core | Oracle.ManagedDataAccess.Client | OracleConnection, OracleCommand |
Зверніть увагу на консистентність іменування: кожен провайдер має свій XxxConnection, XxxCommand, XxxDataReader — і всі вони наслідують від одних і тих самих базових класів (DbConnection, DbCommand, DbDataReader). Це не випадковість, а продуманий архітектурний дизайн.
Одна з найважливіших архітектурних ідей ADO.NET — це провайдерна модель (Provider Model). Суть її в тому, що ADO.NET визначає набір інтерфейсів та абстрактних класів, які описують «контракт» — що повинен уміти будь-який провайдер даних. А конкретні провайдери (для SQL Server, PostgreSQL тощо) реалізують ці контракти.
Це класичний приклад патерну Стратегія (Strategy Pattern) та принципу Інверсії Залежностей (Dependency Inversion Principle, DIP) — ваш код залежить від абстракцій, а не від конкретних реалізацій.
Аналогія: Уявіть розетку на стіні (інтерфейс IDbConnection). Ви знаєте, що в неї можна вставити вилку (викликати Open()). Вам не потрібно знати, як електростанція генерує електрику (внутрішня реалізація SqlConnection). Вам достатньо знати контракт — форма вилки та напруга. Якщо завтра електростанція зміниться з вугільної на сонячну, ваш чайник працюватиме так само — розетка не змінилася.
ADO.NET визначає наступні ключові інтерфейси у просторі імен System.Data:
Open(), Close(), властивість ConnectionString, а також BeginTransaction() для управління транзакціями.CommandText (SQL-запит), CommandType, Parameters, а також методи виконання: ExecuteReader(), ExecuteNonQuery(), ExecuteScalar().IDataReader визначає Read() для переходу до наступного рядка, IDataRecord — методи доступу до полів (GetString(), GetInt32() тощо).Fill() для завантаження та Update() для збереження змін.ParameterName, Value, DbType, Direction.Commit() та Rollback().Окрім інтерфейсів, ADO.NET містить абстрактні базові класи у просторі імен System.Data.Common. Ці класи реалізують спільну логіку, яка однакова для всіх провайдерів, залишаючи специфічні деталі для конкретних реалізацій:
Ця тришарова архітектура інтерфейс → абстрактний клас → конкретна реалізація забезпечує:
DbConnection замість SqlConnection, і він буде працювати з будь-якою СУБД.DbConnection, DbCommand тощо.OpenAsync(), ExecuteReaderAsync()), яка автоматично доступна всім провайдерам.IDbConnection). Абстрактні класи (DbConnection) з'явилися пізніше, у .NET 2.0, коли Microsoft додав підтримку асинхронності та фабрики провайдерів. У сучасному коді рекомендується використовувати абстрактні класи (DbConnection, DbCommand тощо) замість інтерфейсів, оскільки вони містять більше функціональності (зокрема async-методи).Перш ніж писати код, потрібно встановити провайдер для вашої СУБД. У нашому випадку це Microsoft.Data.SqlClient — сучасний провайдер для MS SQL Server.
Відкрийте термінал і виконайте команду:
dotnet new console -n AdoNetDemo
cd AdoNetDemo
dotnet add package Microsoft.Data.SqlClient
Ця команда додасть залежність у файл проєкту (.csproj). Ви побачите щось на кшталт:
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.*" />
</ItemGroup>
Відкрийте Program.cs і додайте:
using Microsoft.Data.SqlClient;
Console.WriteLine("Microsoft.Data.SqlClient встановлено успішно!");
Console.WriteLine($"Версія збірки: {typeof(SqlConnection).Assembly.GetName().Version}");
Запустіть проєкт:
dotnet run
Якщо ви бачите версію збірки — все готово для роботи з ADO.NET.
Microsoft.Data.SqlClient, а не System.Data.SqlClient?System.Data.SqlClient — це вбудований провайдер, який був частиною .NET Framework. З переходом на .NET Core / .NET 5+ Microsoft виніс провайдер у окремий NuGet-пакет Microsoft.Data.SqlClient. Це дозволяє:System.Data.SqlClient більше не отримує нових функцій і підтримується лише в режимі безпекових патчів. Для нових проєктів завжди використовуйте Microsoft.Data.SqlClient.Тепер, коли ми розуміємо архітектуру, давайте напишемо наш перший реальний приклад. Ми підключимося до SQL Server, виконаємо простий SELECT-запит та виведемо результати.
Для цього прикладу ми будемо використовувати базу даних, що містить таблицю Products:
CREATE DATABASE ShopDb;
GO
USE ShopDb;
GO
CREATE TABLE Products (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
Quantity INT NOT NULL DEFAULT 0
);
INSERT INTO Products (Name, Price, Quantity) VALUES
(N'Ноутбук', 25999.99, 15),
(N'Клавіатура', 1299.00, 50),
(N'Мишка', 499.50, 100),
(N'Монітор 27"', 12499.00, 20),
(N'Навушники', 2199.99, 35);
А тепер — C#-код:
using System;
using Microsoft.Data.SqlClient;
// 1. Визначаємо рядок підключення (Connection String)
string connectionString =
"Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
// 2. Створюємо і відкриваємо з'єднання
using SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
Console.WriteLine($"З'єднання встановлено! Стан: {connection.State}");
// 3. Створюємо SQL-команду
string sql = "SELECT Id, Name, Price, Quantity FROM Products WHERE Price > 1000";
using SqlCommand command = new SqlCommand(sql, connection);
// 4. Виконуємо команду та отримуємо DataReader
using SqlDataReader reader = command.ExecuteReader();
// 5. Читаємо результати рядок за рядком
Console.WriteLine("\n{0,-5} {1,-20} {2,12} {3,10}", "ID", "Назва", "Ціна", "Кількість");
Console.WriteLine(new string('-', 50));
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
decimal price = reader.GetDecimal(2);
int quantity = reader.GetInt32(3);
Console.WriteLine($"{id,-5} {name,-20} {price,12:C} {quantity,10}");
}
Console.WriteLine($"\nОброблено рядків: {reader.RecordsAffected}");
Console.WriteLine($"Стан з'єднання після завершення: {connection.State}");
Давайте детально розберемо, що відбувається у кожній частині цього коду:
Рядок 5-6: Рядок підключення (Connection String)
string connectionString =
"Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
Connection String — це спеціальний рядок у форматі ключ=значення;ключ=значення;, який містить усю інформацію для підключення до бази даних:
Server=localhost — адреса SQL Server (може бути IP, hostname або (localdb)\MSSQLLocalDB для LocalDB)Database=ShopDb — назва бази данихTrusted_Connection=True — використовувати Windows Authentication (поточний користувач Windows)TrustServerCertificate=True — довіряти SSL-сертифікату сервера (для розробки)TrustServerCertificate=True підходить лише для локальної розробки. У production-середовищі завжди перевіряйте SSL-сертифікат сервера для захисту від атак «людина посередині» (Man-in-the-Middle, MITM).Рядок 9-11: Створення та відкриття з'єднання
using SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
SqlConnection — це конкретна реалізація DbConnection для MS SQL Server. Зверніть увагу на using-декларацію (C# 8.0+): коли змінна connection вийде із області видимості (scope), автоматично викличеться connection.Dispose(), який закриє з'єднання та поверне його у пул. Це критично важливо — забуте відкрите з'єднання «витікає» з пулу і може вичерпати ліміт підключень.
Метод Open() встановлює TCP-з'єднання з SQL Server, виконує аутентифікацію та обирає базу даних. Якщо сервер недоступний, буде кинуто SqlException.
Рядок 15-16: Створення SQL-команди
string sql = "SELECT Id, Name, Price, Quantity FROM Products WHERE Price > 1000";
using SqlCommand command = new SqlCommand(sql, connection);
SqlCommand приймає два аргументи: SQL-запит і з'єднання, через яке запит буде виконано. Зверніть увагу, що SQL тут написаний як звичайний рядок C# — ADO.NET не перевіряє його синтаксис, це зробить SQL Server при виконанні.
$"SELECT * FROM Products WHERE Name = '{userInput}'" — це вразливість SQL Injection. У наступних статтях ми детально розглянемо параметризовані запити, які вирішують цю проблему.Рядок 19: Виконання запиту
using SqlDataReader reader = command.ExecuteReader();
ExecuteReader() надсилає SQL-запит на SQL Server, вирішує, як його виконати (план запиту), виконує його та повертає SqlDataReader — об'єкт для потокового читання результатів. Дані ще не завантажені в пам'ять — DataReader лише «готовий» до читання.
Рядки 25-33: Читання результатів
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
// ...
}
reader.Read() переміщує внутрішній курсор на наступний рядок результатів і повертає true, якщо рядок існує, або false, якщо рядки закінчилися. Це класичний патерн Iterator — ви обробляєте рядки по одному, не завантажуючи всі в пам'ять одночасно.
Методи GetInt32(0), GetString(1) читають значення конкретного стовпця за його порядковим номером (ordinal, 0-indexed). Це трохи тендітний код, бо залежить від порядку стовпців у SELECT. Альтернативно можна використовувати reader["Name"] або reader.GetOrdinal("Name"), що ми розглянемо у статті про DataReader.
З'єднання встановлено! Стан: Open
ID Назва Ціна Кількість
--------------------------------------------------
1 Ноутбук ₴25 999,99 15
2 Клавіатура ₴1 299,00 50
4 Монітор 27" ₴12 499,00 20
5 Навушники ₴2 199,99 35
Оброблено рядків: -1
Стан з'єднання після завершення: Open
RecordsAffected повертає -1 для SELECT-запитів. Це властивість повертає кількість змінених рядків тільки для INSERT, UPDATE, DELETE. Для SELECT вона завжди повертає -1 — це не помилка, а задумана поведінка.Тепер, коли ми побачили перший приклад, давайте подивимося на загальну картину взаємодії компонентів ADO.NET:
Ця послідовність повторюється в кожному ADO.NET-додатку:
new SqlConnection(connectionString)connection.Open()new SqlCommand(sql, connection)ExecuteReader() / ExecuteNonQuery() / ExecuteScalar()while (reader.Read()) { ... }Dispose() (автоматично через using)Це фундаментальний шаблон, який ви будете використовувати знову і знову. У наступних статтях ми детально розглянемо кожен з цих компонентів.
Створіть програму, яка:
SELECT @@VERSION). Використайте ExecuteScalar().SqlException.Message, SqlException.Number).Створіть програму, яка підключається до SQL Server та виводить:
SELECT @@SERVERNAME)SELECT DB_NAME())SELECT SUSER_SNAME())SELECT GETDATE())Використайте окремий SqlCommand з ExecuteScalar() для кожного запиту.
Створіть базу даних CatalogDb з таблицею Categories (Id, Name, Description) та Products (Id, Name, Price, CategoryId). Заповніть тестовими даними (мінімум 10 продуктів у 3 категоріях). Напишіть програму, яка:
Напишіть програму, яка підключається до SQL Server і виводить список усіх баз даних на сервері (використайте запит SELECT name FROM sys.databases). Для кожної бази виведіть її назву та розмір (підказка: використайте системну процедуру sp_databases або запит до sys.master_files).
Створіть консольний додаток — простий SQL-клієнт:
exit завершує програму.Зверніть увагу на обробку помилок — невалідний SQL не повинен «роняти» програму.
Створіть проєкт, в якому ви:
PrintDatabaseInfo(DbConnection connection), який приймає абстрактний DbConnection (не конкретний SqlConnection).connection.GetType().Name), версію сервера (connection.ServerVersion) та стан.SqlConnection.DbConnection замість SqlConnection є кращою практикою.ADO.NET — фундамент
Провайдерна модель
Connected vs Disconnected
Microsoft.Data.SqlClient
System.DataDbConnection — рядок підключення, пул з'єднань (Connection Pooling), обробку помилок та best practices управління з'єднаннями.