У першій статті ми згадували, що ADO.NET має два режими роботи: з'єднаний (connected) та від'єднаний (disconnected). Усі попередні статті були присвячені з'єднаному режиму — SqlConnection → SqlCommand → SqlDataReader. DataReader працює, поки з'єднання відкрите, і після закриття дані недоступні.
Від'єднаний режим пропонує інший підхід: ви завантажуєте дані з бази в об'єкт у пам'яті (DataSet / DataTable), закриваєте з'єднання і працюєте з даними офлайн — переглядаєте, фільтруєте, сортуєте, змінюєте. Потім, коли готові, відкриваєте з'єднання знову і синхронізуєте зміни назад у базу.
Аналогія: З'єднаний режим (DataReader) — це як переглядати відео онлайн (streaming). Від'єднаний режим (DataSet) — це як завантажити відео на телефон і дивитися офлайн. Ви можете перемотувати, зупиняти, повертатися — все це без інтернету.
📡 Connected Mode
📦 Disconnected Mode
Ключові об'єкти:
DataTable — це центральний об'єкт від'єднаного режиму. Він представляє одну таблицю з рядками та стовпцями, подібно до таблиці в базі даних.
using System;
using System.Data;
// Створюємо DataTable
DataTable productsTable = new DataTable("Products");
// Додаємо стовпці (DataColumn)
DataColumn idColumn = new DataColumn("Id", typeof(int))
{
AutoIncrement = true, // AUTO_INCREMENT
AutoIncrementSeed = 1, // Початкове значення
AutoIncrementStep = 1, // Крок
AllowDBNull = false,
Unique = true
};
productsTable.Columns.Add(idColumn);
productsTable.Columns.Add(new DataColumn("Name", typeof(string))
{
MaxLength = 100,
AllowDBNull = false
});
productsTable.Columns.Add(new DataColumn("Price", typeof(decimal))
{
AllowDBNull = false,
DefaultValue = 0m
});
productsTable.Columns.Add(new DataColumn("Quantity", typeof(int))
{
AllowDBNull = false,
DefaultValue = 0
});
productsTable.Columns.Add(new DataColumn("Description", typeof(string))
{
MaxLength = 500,
AllowDBNull = true // nullable
});
// Встановлюємо первинний ключ
productsTable.PrimaryKey = new[] { idColumn };
Console.WriteLine($"Таблиця: {productsTable.TableName}");
Console.WriteLine($"Стовпців: {productsTable.Columns.Count}");
Console.WriteLine($"Рядків: {productsTable.Rows.Count}");
Розбір коду:
DataTable("Products") — ім'я таблиці (аналог CREATE TABLE Products).DataColumn з властивостями AutoIncrement, AllowDBNull, Unique — це аналоги SQL-обмежень IDENTITY, NOT NULL, UNIQUE.PrimaryKey — масив DataColumn, що складають первинний ключ.// Спосіб 1: NewRow() + Add()
DataRow row1 = productsTable.NewRow();
row1["Name"] = "Ноутбук ASUS";
row1["Price"] = 32999.99m;
row1["Quantity"] = 15;
row1["Description"] = "15.6\", i7, 16GB RAM";
productsTable.Rows.Add(row1);
// Спосіб 2: Add() з масивом значень
// null для AutoIncrement стовпця — значення згенерується автоматично
productsTable.Rows.Add(null, "Мишка Logitech", 1299.99m, 50, null);
productsTable.Rows.Add(null, "Монітор LG", 12500m, 8, "27\", 4K IPS");
// Читання даних
Console.WriteLine("\n=== Усі товари ===");
foreach (DataRow row in productsTable.Rows)
{
int id = (int)row["Id"]; // Приведення типу обов'язкове!
string name = (string)row["Name"];
decimal price = (decimal)row["Price"];
int qty = (int)row["Quantity"];
// Перевірка NULL
string description = row["Description"] == DBNull.Value
? "(без опису)"
: (string)row["Description"];
Console.WriteLine($" [{id}] {name}: {price:C} x{qty} — {description}");
}
// Find() шукає за PrimaryKey — O(1) завдяки внутрішньому індексу
DataRow? found = productsTable.Rows.Find(2); // Id = 2
if (found != null)
{
Console.WriteLine($"Знайдено: {found["Name"]}");
}
else
{
Console.WriteLine("Товар не знайдено.");
}
// Select() з фільтром та сортуванням
DataRow[] expensive = productsTable.Select(
"Price > 5000", // фільтр (синтаксис Expression)
"Price DESC" // сортування
);
Console.WriteLine($"\nТовари дорожче 5000 ₴ ({expensive.Length}):");
foreach (DataRow row in expensive)
{
Console.WriteLine($" {row["Name"]}: {row["Price"]:C}");
}
// Складніші фільтри
DataRow[] filtered = productsTable.Select(
"Name LIKE 'Ноутбук*' AND Quantity > 10");
// Обчислювані вирази
object totalValue = productsTable.Compute("SUM(Price * Quantity)", "");
Console.WriteLine($"\nЗагальна вартість складу: {totalValue:C}");
object avgPrice = productsTable.Compute("AVG(Price)", "Price > 1000");
Console.WriteLine($"Середня ціна (> 1000 ₴): {avgPrice:C}");
Кожен DataRow має властивість RowState, яка відстежує стан рядка:
NewRow(), але ще не додано до таблиці.AcceptChanges().// Демонстрація RowState
DataRow newRow = productsTable.NewRow();
Console.WriteLine($"Після NewRow(): {newRow.RowState}"); // Detached
productsTable.Rows.Add(newRow);
Console.WriteLine($"Після Add(): {newRow.RowState}"); // Added
productsTable.AcceptChanges(); // "Прийняти" зміни — скинути стан
Console.WriteLine($"Після AcceptChanges(): {newRow.RowState}"); // Unchanged
newRow["Name"] = "Нова назва";
Console.WriteLine($"Після зміни: {newRow.RowState}"); // Modified
// Перегляд оригінального та поточного значення
Console.WriteLine($" Original: {newRow["Name", DataRowVersion.Original]}");
Console.WriteLine($" Current: {newRow["Name", DataRowVersion.Current]}");
newRow.Delete();
Console.WriteLine($"Після Delete(): {newRow.RowState}"); // Deleted
// RejectChanges() — скасувати всі зміни, повернутися до Original
// productsTable.RejectChanges();
Чому це важливо? DataAdapter (наступна стаття) використовує RowState для визначення, які SQL-команди виконати: Added → INSERT, Modified → UPDATE, Deleted → DELETE, Unchanged → пропустити.
DataSet — це контейнер для кількох DataTable з підтримкою зв'язків (DataRelation) між ними:
using System;
using System.Data;
// Створюємо DataSet — "міні-базу даних"
DataSet shopDb = new DataSet("ShopDB");
// Таблиця Categories
DataTable categories = new DataTable("Categories");
categories.Columns.Add("Id", typeof(int));
categories.Columns.Add("Name", typeof(string));
categories.PrimaryKey = new[] { categories.Columns["Id"]! };
// Таблиця Products
DataTable products = new DataTable("Products");
products.Columns.Add("Id", typeof(int));
products.Columns.Add("Name", typeof(string));
products.Columns.Add("Price", typeof(decimal));
products.Columns.Add("CategoryId", typeof(int)); // FK
products.PrimaryKey = new[] { products.Columns["Id"]! };
// Додаємо таблиці до DataSet
shopDb.Tables.Add(categories);
shopDb.Tables.Add(products);
// Створюємо зв'язок (DataRelation) — аналог FOREIGN KEY
DataRelation relation = new DataRelation(
"FK_Products_Categories", // Ім'я зв'язку
categories.Columns["Id"]!, // Parent column (PK)
products.Columns["CategoryId"]!, // Child column (FK)
createConstraints: true // Автоматично створити ForeignKeyConstraint
);
shopDb.Relations.Add(relation);
// Заповнюємо даними
categories.Rows.Add(1, "Ноутбуки");
categories.Rows.Add(2, "Периферія");
categories.Rows.Add(3, "Монітори");
products.Rows.Add(1, "ASUS ZenBook", 32000m, 1);
products.Rows.Add(2, "Dell XPS", 45000m, 1);
products.Rows.Add(3, "Мишка Logitech", 1300m, 2);
products.Rows.Add(4, "LG 27\" 4K", 15000m, 3);
// AcceptChanges — скинути RowState всіх рядків на Unchanged
shopDb.AcceptChanges();
// Навігація по зв'язку: Category → Products
Console.WriteLine("=== Категорії та їхні товари ===");
foreach (DataRow catRow in categories.Rows)
{
Console.WriteLine($"\n📁 {catRow["Name"]}:");
// GetChildRows() повертає дочірні рядки за зв'язком
DataRow[] childProducts = catRow.GetChildRows("FK_Products_Categories");
foreach (DataRow prodRow in childProducts)
{
Console.WriteLine($" 📦 {prodRow["Name"]}: {prodRow["Price"]:C}");
}
}
// Навігація у зворотному напрямку: Product → Category
DataRow laptop = products.Rows.Find(1)!;
DataRow parentCategory = laptop.GetParentRow("FK_Products_Categories")!;
Console.WriteLine($"\n{laptop["Name"]} належить до категорії: {parentCategory["Name"]}");
Розбір коду:
DataRelation — зв'язок між Categories.Id (батьківський) та Products.CategoryId (дочірній). createConstraints: true додає ForeignKeyConstraint, яке не дозволить додати продукт з неіснуючою категорією.catRow.GetChildRows("FK_Products_Categories") — повертає всі продукти цієї категорії. Це аналог JOIN у SQL, але виконується в пам'яті.GetParentRow() — зворотна навігація: від продукту до його категорії.DataTable підтримує два типи обмежень:
using System.Data;
DataTable employees = new DataTable("Employees");
employees.Columns.Add("Id", typeof(int));
employees.Columns.Add("Email", typeof(string));
employees.Columns.Add("DepartmentId", typeof(int));
// UniqueConstraint — аналог UNIQUE у SQL
employees.Constraints.Add(
new UniqueConstraint("UQ_Email", employees.Columns["Email"]!));
// PrimaryKey неявно створює UniqueConstraint
// ForeignKeyConstraint — створюється автоматично через DataRelation
// Але можна створити вручну:
DataTable departments = new DataTable("Departments");
departments.Columns.Add("Id", typeof(int));
departments.PrimaryKey = new[] { departments.Columns["Id"]! };
ForeignKeyConstraint fk = new ForeignKeyConstraint(
"FK_Employees_Departments",
departments.Columns["Id"]!, // parent
employees.Columns["DepartmentId"]! // child
);
// Правила каскадного видалення/оновлення
fk.DeleteRule = Rule.Cascade; // Видалити department → видалити employees
fk.UpdateRule = Rule.Cascade; // Змінити department.Id → оновити employees.DeptId
fk.AcceptRejectRule = AcceptRejectRule.Cascade;
employees.Constraints.Add(fk);
DataSet має вбудовану підтримку XML:
// Запис у XML
shopDb.WriteXml("shop_data.xml", XmlWriteMode.WriteSchema);
Console.WriteLine("✅ Дані збережено у shop_data.xml");
// Читання з XML
DataSet loaded = new DataSet();
loaded.ReadXml("shop_data.xml", XmlReadMode.ReadSchema);
Console.WriteLine($"Завантажено таблиць: {loaded.Tables.Count}");
foreach (DataTable table in loaded.Tables)
{
Console.WriteLine($" {table.TableName}: {table.Rows.Count} рядків");
}
using System.Text.Json;
static string DataTableToJson(DataTable table)
{
var rows = new List<Dictionary<string, object?>>();
foreach (DataRow row in table.Rows)
{
var dict = new Dictionary<string, object?>();
foreach (DataColumn col in table.Columns)
{
dict[col.ColumnName] = row[col] == DBNull.Value ? null : row[col];
}
rows.Add(dict);
}
return JsonSerializer.Serialize(rows, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
string json = DataTableToJson(products);
Console.WriteLine(json);
До цього моменту ми заповнювали DataTable вручну. У реальних додатках дані завантажуються з бази через SqlDataReader:
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
DataTable LoadProducts()
{
DataTable table = new DataTable("Products");
using SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(
"SELECT Id, Name, Price, Quantity, Description FROM Products ORDER BY Name",
connection);
using SqlDataReader reader = command.ExecuteReader();
// Load() заповнює DataTable з DataReader
table.Load(reader);
return table;
}
DataTable products = LoadProducts();
Console.WriteLine($"Завантажено {products.Rows.Count} товарів:");
foreach (DataRow row in products.Rows)
{
Console.WriteLine($" [{row["Id"]}] {row["Name"]}: {row["Price"]:C}");
}
// Фільтрація та сортування в пам'яті — без SQL!
DataRow[] cheap = products.Select("Price < 5000", "Name ASC");
Console.WriteLine($"\nДешеві товари ({cheap.Length}):");
foreach (DataRow row in cheap)
{
Console.WriteLine($" {row["Name"]}: {row["Price"]:C}");
}
Розбір коду:
table.Load(reader) — ключовий метод. Він автоматично:
DataColumn для кожного стовпця в результатах (з правильними типами).Load() з'єднання можна закрити — дані вже в пам'яті.DataView — це «лінза» для DataTable, що дозволяє фільтрувати та сортувати дані без зміни оригінальної таблиці:
using System.Data;
// Створюємо DataView для таблиці Products
DataView view = new DataView(products);
// Фільтр і сортування
view.RowFilter = "Price > 1000 AND Quantity > 0";
view.Sort = "Price DESC";
Console.WriteLine($"Відфільтровано {view.Count} рядків з {products.Rows.Count}:");
foreach (DataRowView rowView in view)
{
Console.WriteLine($" {rowView["Name"]}: {rowView["Price"]:C}");
}
// Пошук у DataView (якщо Sort встановлено)
int idx = view.Find(15000m); // Шукає значення Price = 15000 у відсортованому view
if (idx >= 0)
{
Console.WriteLine($"\nЗнайдено: {view[idx]["Name"]}");
}
// DataView підтримує прив'язку до UI (DataBinding)
// dataGridView.DataSource = view; // WinForms
// productsListView.ItemsSource = view; // WPF
DataColumn може містити вираз, який обчислюється автоматично:
// Вартість = Price * Quantity (обчислюється автоматично)
DataColumn totalColumn = new DataColumn("TotalValue", typeof(decimal))
{
Expression = "Price * Quantity"
};
products.Columns.Add(totalColumn);
// Знижка (умовна)
DataColumn discountColumn = new DataColumn("DiscountedPrice", typeof(decimal))
{
Expression = "IIF(Price > 10000, Price * 0.9, Price)"
};
products.Columns.Add(discountColumn);
// Відображення
foreach (DataRow row in products.Rows)
{
Console.WriteLine($" {row["Name"]}: {row["Price"]:C} → {row["DiscountedPrice"]:C} (всього: {row["TotalValue"]:C})");
}
object, потрібне приведення типуСтворіть DataTable Students (Id, FirstName, LastName, BirthDate, GPA) програмно. Додайте 5 студентів. Реалізуйте: пошук за Id, фільтрацію за GPA > 3.5, обчислення середнього GPA через Compute().
Завантажте таблицю Products з бази в DataTable через DataReader + Load(). Використайте DataView для відображення: (1) усіх товарів за ціною DESC, (2) товарів з ціною > 1000, (3) пошук за ім'ям.
Створіть DataSet з трьома таблицями: Departments, Employees, Projects. Налаштуйте DataRelation між ними. Реалізуйте навігацію: «Всі працівники відділу X», «Відділ працівника Y», «Проєкти відділу Z».
Завантажте дані з бази в DataSet, збережіть у XML (WriteXml). Модифікуйте XML-файл вручну (додайте рядок). Завантажте назад через ReadXml. Виведіть різницю між оригіналом і завантаженими даними.
Реалізуйте систему відстеження змін на базі RowState:
GetChanges() повертає нову DataTable лише зі зміненими рядками.Створіть клас QueryEngine, що працює з DataTable:
engine.From("Products").Where("Price > 1000").OrderBy("Name").Top(10).Execute()Select() та Compute() під капотом.IEnumerable<DataRow> або нового DataTable.DataTable
DataSet
RowState
DataView
Fill(), Update(), автоматичне генерування INSERT/UPDATE/DELETE команд через SqlCommandBuilder, та batch-оновлення.