У попередній статті ми навчилися працювати з DataTable та DataSet — завантажувати дані в пам'ять, фільтрувати, сортувати, змінювати. Але залишилось критичне питання: як зберегти зміни назад у базу даних?
Ми знаємо, що DataRow зберігає свій RowState: Added (нові рядки), Modified (змінені), Deleted (видалені). Але DataTable сама по собі не знає, як спілкуватися з SQL Server. Їй потрібен посередник — DataAdapter.
DataAdapter — це «міст» між від'єднаним DataSet/DataTable та базою даних. Він виконує дві основні операції:
Аналогія: DataAdapter — це як кур'єрська служба між вашим офісом (DataTable) та складом (база даних). Fill() — кур'єр привозить товари зі складу у ваш офіс. Update() — кур'єр забирає замовлення з офісу та доставляє їх на склад. Кур'єр не зберігає товари — він лише транспортує.
DataAdapter містить чотири команди:
Fill()). Зазвичай SELECT * FROM Table.Added). Зазвичай INSERT INTO Table (...) VALUES (...).Modified). Зазвичай UPDATE Table SET ... WHERE Id = @Id.Deleted). Зазвичай DELETE FROM Table WHERE Id = @Id.Fill() виконує SelectCommand і заповнює DataTable результатами:
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
// Створюємо DataAdapter з SELECT-запитом
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price, Quantity FROM Products ORDER BY Name",
connectionString);
// Створюємо DataTable та заповнюємо
DataTable products = new DataTable("Products");
int rowsLoaded = adapter.Fill(products);
Console.WriteLine($"Завантажено {rowsLoaded} рядків.");
Console.WriteLine($"Стовпців: {products.Columns.Count}");
foreach (DataRow row in products.Rows)
{
Console.WriteLine($" [{row["Id"]}] {row["Name"]}: {row["Price"]:C} — RowState: {row.RowState}");
}
Розбір коду:
SqlDataAdapter(sql, connectionString) — зручна перевантажка, яка автоматично створює SelectCommand та SqlConnection.adapter.Fill(products) — виконує SELECT, створює стовпці в DataTable (якщо їх немає), завантажує рядки. Повертає кількість завантажених рядків.Fill() всі рядки мають RowState = Unchanged — DataAdapter автоматично викликає AcceptChanges().DataSet shopDb = new DataSet("ShopDB");
// Адаптер для Products
SqlDataAdapter productAdapter = new SqlDataAdapter(
"SELECT * FROM Products", connectionString);
productAdapter.Fill(shopDb, "Products"); // Ім'я таблиці в DataSet
// Адаптер для Categories
SqlDataAdapter categoryAdapter = new SqlDataAdapter(
"SELECT * FROM Categories", connectionString);
categoryAdapter.Fill(shopDb, "Categories");
Console.WriteLine($"Таблиць у DataSet: {shopDb.Tables.Count}");
foreach (DataTable table in shopDb.Tables)
{
Console.WriteLine($" {table.TableName}: {table.Rows.Count} рядків");
}
Update() — найцікавіший метод DataAdapter. Він проходить по всіх рядках DataTable і виконує відповідну SQL-команду на основі RowState:
| RowState | Команда DataAdapter |
|---|---|
Added | InsertCommand |
Modified | UpdateCommand |
Deleted | DeleteCommand |
Unchanged | Пропускається |
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
SqlConnection connection = new SqlConnection(connectionString);
// Адаптер з SELECT
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price, Quantity FROM Products ORDER BY Name",
connection);
// INSERT-команда
adapter.InsertCommand = new SqlCommand(@"
INSERT INTO Products (Name, Price, Quantity)
VALUES (@Name, @Price, @Quantity);
SET @Id = SCOPE_IDENTITY();",
connection);
adapter.InsertCommand.Parameters.Add("@Name", SqlDbType.NVarChar, 100, "Name");
adapter.InsertCommand.Parameters.Add("@Price", SqlDbType.Decimal, 0, "Price");
adapter.InsertCommand.Parameters.Add("@Quantity", SqlDbType.Int, 0, "Quantity");
// OUTPUT-параметр для отримання нового Id
SqlParameter idOutParam = adapter.InsertCommand.Parameters.Add("@Id", SqlDbType.Int);
idOutParam.Direction = ParameterDirection.Output;
idOutParam.SourceColumn = "Id";
// UPDATE-команда
adapter.UpdateCommand = new SqlCommand(@"
UPDATE Products
SET Name = @Name, Price = @Price, Quantity = @Quantity
WHERE Id = @Id",
connection);
adapter.UpdateCommand.Parameters.Add("@Name", SqlDbType.NVarChar, 100, "Name");
adapter.UpdateCommand.Parameters.Add("@Price", SqlDbType.Decimal, 0, "Price");
adapter.UpdateCommand.Parameters.Add("@Quantity", SqlDbType.Int, 0, "Quantity");
adapter.UpdateCommand.Parameters.Add("@Id", SqlDbType.Int, 0, "Id");
// DELETE-команда
adapter.DeleteCommand = new SqlCommand(
"DELETE FROM Products WHERE Id = @Id",
connection);
adapter.DeleteCommand.Parameters.Add("@Id", SqlDbType.Int, 0, "Id");
// Завантажуємо дані
DataTable products = new DataTable("Products");
adapter.Fill(products);
Console.WriteLine($"Завантажено {products.Rows.Count} товарів.");
// Вносимо зміни в пам'яті
DataRow newRow = products.NewRow();
newRow["Name"] = "Клавіатура механічна";
newRow["Price"] = 2500m;
newRow["Quantity"] = 25;
products.Rows.Add(newRow);
// Змінюємо існуючий рядок
DataRow? existingRow = products.Rows.Find(1); // Потребує PrimaryKey
if (existingRow != null)
{
existingRow["Price"] = Convert.ToDecimal(existingRow["Price"]) * 1.1m; // +10%
}
// Видаляємо рядок
if (products.Rows.Count > 3)
{
products.Rows[products.Rows.Count - 1].Delete();
}
// Синхронізуємо з базою — один виклик!
int affectedRows = adapter.Update(products);
Console.WriteLine($"\n✅ Синхронізовано {affectedRows} рядків з базою.");
Розбір коду:
Add() — це SourceColumn. Він вказує, з якого стовпця DataTable брати значення для параметра. Наприклад, "Name" означає: для кожного рядка значення @Name береться з row["Name"].@Id з SourceColumn = "Id" — після INSERT нове значення Id записується назад у DataRow.adapter.Update(products) — DataAdapter ітерує по рядках:
newRow (Added) → виконує InsertCommandexistingRow (Modified) → виконує UpdateCommandAcceptChanges().Ручне створення InsertCommand/UpdateCommand/DeleteCommand — це багато коду. SqlCommandBuilder може автоматично згенерувати ці команди на основі SelectCommand:
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
// Адаптер з SELECT
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price, Quantity FROM Products",
connectionString);
// CommandBuilder автоматично генерує INSERT, UPDATE, DELETE!
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
// Перегляд згенерованих команд
Console.WriteLine("INSERT: " + builder.GetInsertCommand().CommandText);
Console.WriteLine("UPDATE: " + builder.GetUpdateCommand().CommandText);
Console.WriteLine("DELETE: " + builder.GetDeleteCommand().CommandText);
// Завантажуємо дані
DataTable products = new DataTable("Products");
adapter.Fill(products);
// Встановлюємо PrimaryKey (для Find())
products.PrimaryKey = new[] { products.Columns["Id"]! };
// Додаємо новий товар
DataRow newRow = products.NewRow();
newRow["Name"] = "Веб-камера HD";
newRow["Price"] = 1500m;
newRow["Quantity"] = 30;
products.Rows.Add(newRow);
// Оновлюємо існуючий
DataRow? row = products.Rows.Find(1);
if (row != null) row["Price"] = 99999m;
// Синхронізуємо — CommandBuilder забезпечує все автоматично!
int affected = adapter.Update(products);
Console.WriteLine($"\n✅ Оновлено {affected} рядків.");
Розбір коду:
SqlCommandBuilder(adapter) — аналізує SelectCommand і генерує INSERT/UPDATE/DELETE. Вимоги:
SqlCommandBuilder має обмеження:DataAdapter може перейменувати стовпці при завантаженні з бази в DataTable:
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT ProductID, ProductName, UnitPrice FROM Products",
connectionString);
// Маппінг: стовпці SQL → стовпці DataTable
adapter.TableMappings.Add("Table", "Products");
adapter.TableMappings[0].ColumnMappings.Add("ProductID", "Id");
adapter.TableMappings[0].ColumnMappings.Add("ProductName", "Name");
adapter.TableMappings[0].ColumnMappings.Add("UnitPrice", "Price");
DataTable products = new DataTable("Products");
adapter.Fill(products);
// У DataTable стовпці мають «чисті» імена
foreach (DataColumn col in products.Columns)
{
Console.WriteLine($" {col.ColumnName} ({col.DataType.Name})");
// Id (Int32), Name (String), Price (Decimal)
}
Коли два користувачі одночасно змінюють один рядок, виникає конфлікт. DataAdapter обробляє це через подію RowUpdated:
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price FROM Products", connectionString);
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
// Обробка помилок під час Update
adapter.RowUpdated += (sender, e) =>
{
if (e.Status == UpdateStatus.ErrorsOccurred)
{
Console.WriteLine($"❌ Помилка при оновленні рядка: {e.Errors?.Message}");
Console.WriteLine($" Рядок: {e.Row["Name"]}");
// Варіанти обробки:
e.Status = UpdateStatus.SkipCurrentRow; // Пропустити цей рядок
// e.Status = UpdateStatus.Continue; // Продовжити решту
// e.Status = UpdateStatus.SkipAllRemainingRows; // Зупинити все
}
else if (e.RecordsAffected == 0 && e.StatementType == StatementType.Update)
{
// Жоден рядок не оновлено — конфлікт (рядок змінився/видалився)
Console.WriteLine($"⚠️ Конфлікт оновлення для: {e.Row["Name"]}");
e.Status = UpdateStatus.SkipCurrentRow;
}
};
// Завантажуємо та модифікуємо
DataTable products = new DataTable();
adapter.Fill(products);
// Симулюємо конфлікт — змінюємо рядок, який хтось інший вже змінив
if (products.Rows.Count > 0)
{
products.Rows[0]["Price"] = 999999m;
adapter.Update(products);
}
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
// Створюємо адаптер з автогенерацією команд
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price, Quantity FROM Products ORDER BY Name",
connectionString);
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
// Завантажуємо
DataTable products = new DataTable("Products");
adapter.Fill(products);
products.PrimaryKey = new[] { products.Columns["Id"]! };
PrintTable(products, "Початковий стан");
// === CREATE ===
DataRow newRow = products.NewRow();
newRow["Name"] = "USB-хаб";
newRow["Price"] = 750m;
newRow["Quantity"] = 100;
products.Rows.Add(newRow);
// === UPDATE ===
DataRow? laptop = products.Select("Name LIKE 'Ноутбук*'").FirstOrDefault();
if (laptop != null)
{
laptop["Price"] = Convert.ToDecimal(laptop["Price"]) * 0.9m; // -10%
Console.WriteLine($"\n💰 Знижка на {laptop["Name"]}: нова ціна {laptop["Price"]:C}");
}
// === DELETE ===
DataRow[] cheapItems = products.Select("Price < 500");
foreach (DataRow row in cheapItems)
{
Console.WriteLine($"\n🗑️ Видаляємо: {row["Name"]} ({row["Price"]:C})");
row.Delete();
}
// Перегляд змін перед збереженням
DataTable? changes = products.GetChanges();
if (changes != null)
{
Console.WriteLine($"\n📝 Зміни для збереження ({changes.Rows.Count} рядків):");
foreach (DataRow row in changes.Rows)
{
Console.WriteLine($" RowState: {row.RowState}");
}
}
// === ЗБЕРЕЖЕННЯ в базу ===
int affected = adapter.Update(products);
Console.WriteLine($"\n✅ Збережено {affected} змін у базі.");
// Перезавантаження для підтвердження
products.Clear();
adapter.Fill(products);
PrintTable(products, "Після збереження");
// Хелпер для виводу таблиці
void PrintTable(DataTable table, string title)
{
Console.WriteLine($"\n=== {title} ({table.Rows.Count} рядків) ===");
foreach (DataRow row in table.Rows)
{
Console.WriteLine($" [{row["Id"]}] {row["Name"]}: {row["Price"]:C} x{row["Quantity"]}");
}
}
Розбір коду:
products.GetChanges() — повертає нову DataTable лише зі зміненими рядками. Корисно для перегляду або логування перед збереженням.adapter.Update(products) — один виклик синхронізує всі три типи змін (INSERT, UPDATE, DELETE).За замовчуванням DataAdapter виконує окрему SQL-команду для кожного рядка. Для великої кількості змін це повільно. Властивість UpdateBatchSize дозволяє об'єднати кілька команд у один batch:
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price FROM Products", connectionString);
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
// Пакетне оновлення — до 50 команд за раз
adapter.UpdateBatchSize = 50;
// 0 = необмежений розмір batch
// 1 = по одному рядку (за замовчуванням)
DataTable products = new DataTable();
adapter.Fill(products);
// Додаємо 200 нових рядків
for (int i = 0; i < 200; i++)
{
DataRow row = products.NewRow();
row["Name"] = $"Товар {i + 1}";
row["Price"] = 100m + i * 10;
products.Rows.Add(row);
}
// Batch update — набагато швидше!
int affected = adapter.Update(products);
Console.WriteLine($"Batch update: {affected} рядків за менше звернень до бази.");
using System;
using System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;";
using SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT Id, Name, Price, Quantity FROM Products", connection);
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
DataTable products = new DataTable();
adapter.Fill(products);
// Вносимо зміни
products.Rows.Add(null, "Тест-транзакція-1", 100m, 10);
products.Rows.Add(null, "Тест-транзакція-2", 200m, 20);
// Update у транзакції
using SqlTransaction transaction = connection.BeginTransaction();
try
{
// Прив'язуємо всі команди до транзакції
adapter.InsertCommand = builder.GetInsertCommand();
adapter.UpdateCommand = builder.GetUpdateCommand();
adapter.DeleteCommand = builder.GetDeleteCommand();
adapter.InsertCommand.Transaction = transaction;
adapter.UpdateCommand.Transaction = transaction;
adapter.DeleteCommand.Transaction = transaction;
int affected = adapter.Update(products);
transaction.Commit();
Console.WriteLine($"✅ {affected} рядків збережено в транзакції.");
}
catch (Exception ex)
{
transaction.Rollback();
products.RejectChanges(); // Повертаємо DataTable до стану до змін
Console.WriteLine($"❌ Rollback: {ex.Message}");
}
Створіть програму, яка:
Products через SqlDataAdapter.Fill().DataView для відображення: (а) за ціною DESC, (б) лише дорогі (> 5000).Створіть інтерактивну консольну програму з меню:
adapter.Update()).RejectChanges()).
Використовуйте SqlCommandBuilder для автогенерації команд.Завантажте Categories та Products в один DataSet. Налаштуйте DataRelation. Реалізуйте навігацію: вибір категорії → показ товарів категорії. Дозвольте додавати товари до обраної категорії та зберігати через DataAdapter.
Створіть метод ImportFromCsv(string csvPath, string tableName):
adapter.Update() з UpdateBatchSize = 100.Реалізуйте Repository, що працює офлайн:
Fill() всіх даних у DataSet.Sync() — відкриває з'єднання та Update().Refresh() — перезавантажує дані з бази.RowUpdated.Створіть систему звітування про зміни:
GenerateChangeReport() аналізує GetChanges() та повертає звіт:
SqlDataAdapter
SqlCommandBuilder
Batch Update
Обробка конфліктів
Ви пройшли повний шлях від базових концепцій до advanced-патернів ADO.NET:
Зрозуміли two-tier архітектуру: Connected (DataReader) та Disconnected (DataSet) режими.
Навчилися створювати, налаштовувати та ефективно використовувати з'єднання з Connection Pooling.
Освоїли ExecuteReader, ExecuteNonQuery, ExecuteScalar для всіх типів SQL-операцій.
Зрозуміли forward-only cursor, типізований доступ, маппінг на POCO.
Навчились безпечній передачі даних через SqlParameter та виклику збережених процедур.
Освоїли транзакції, рівні ізоляції, savepoints та обробку deadlock.
Зрозуміли паттерн Abstract Factory для роботи з будь-якою СУБД.
Навчились використовувати async/await для ефективного I/O та IAsyncEnumerable.
Освоїли DataTable, DataRow, RowState для роботи з даними в пам'яті.
Зрозуміли Fill/Update механізм для двосторонньої синхронізації DataSet з базою.