C# DataSetとは?使い方・DataTableとの違い・DB連携をサンプルコードで初心者向けに解説
はじめに
C#でデータベースを扱う方法を調べると、DataSet、DataTable、SqlDataAdapterといったクラスがよく登場します。しかし、初心者にとっては「DataSetとDataTableは何が違うのか」「データベースに直接接続するクラスなのか」「現在の.NETでも使う場面があるのか」が分かりにくいかもしれません。
DataSetは、複数の表形式データをメモリ上に保持し、テーブル間の関連や制約、データの変更状態まで管理できるクラスです。ADO.NETでは、データベースなどのデータソースと切り離してデータを扱うための中心的な役割を持ちます。Microsoft Learn+2
Microsoft Learn+2
この記事では、C#のDataSetの基本的な使い方から、DataTableとの違い、検索・編集・削除、複数テーブルの関連付け、SQL Serverとの連携、データベースへの更新、XML変換までをサンプルコード付きで解説します。
1. C#のDataSetとは
1-1. DataSetは複数のテーブルをメモリ上で管理するクラス
DataSetは、複数のDataTableをまとめて保持するためのクラスです。
データベースにたとえると、次のような関係になります。
DataSet:複数のテーブルを格納する入れ物
DataTable:1つのテーブル
DataRow:テーブル内の1行
DataColumn:テーブル内の1列
たとえば、顧客情報を格納するCustomersテーブルと、注文情報を格納するOrdersテーブルを、1つのDataSetにまとめて管理できます。
C#DataSet dataSet = new DataSet("ShopData");
DataTable customers = new DataTable("Customers");
DataTable orders = new DataTable("Orders");
dataSet.Tables.Add(customers);
dataSet.Tables.Add(orders);
DataSetはデータそのものだけでなく、主キー、外部キーに相当する制約、テーブル間の関連も保持できます。そのため、単なる配列やリストよりも、リレーショナルデータベースに近い構造をメモリ上に作成できます。
1-2. DataSetがADO.NETで果たす役割
ADO.NETは、C#などの.NETアプリケーションからデータベースやXMLデータを操作するための仕組みです。
ADO.NETでは、主に次の2つの要素が使われます。
データベースへ接続してSQLを実行するデータプロバイダー
取得したデータをメモリ上で管理するDataSet
SQL Serverを使用する場合は、SqlConnectionやSqlCommand、SqlDataReader、SqlDataAdapterなどがデータプロバイダー側のクラスです。
DataSetはデータソースに直接依存しません。SQL Serverから取得したデータだけでなく、XMLから読み込んだデータや、プログラム内で作成したデータも同じ形式で管理できます。
1-3. DataSetが「非接続型」と呼ばれる理由
DataSetは、データベースとの接続を維持しなくてもデータを操作できるため、「非接続型」と呼ばれます。
一般的な処理の流れは次のとおりです。
データベースへ接続する
必要なデータをDataSetへ読み込む
データベース接続を閉じる
DataSet上で検索や編集を行う
必要になった時点で再接続し、変更をデータベースへ反映する
データを取得した後も接続を開き続ける必要がないため、画面上で時間をかけて編集するデスクトップアプリケーションや、複数テーブルをまとめて扱う処理に適しています。
一方、DataSetに読み込まれたデータは元のデータベースと自動的に同期されるわけではありません。別の利用者がデータベースを更新しても、DataSet内のデータは再取得するまで古い状態のままです。
1-4. DataSet・DataTable・DataRow・DataColumnの関係
DataSetを構成する主なクラスの関係は次のとおりです。
DataSet
├─ DataTable: Customers
│ ├─ DataColumn: CustomerId
│ ├─ DataColumn: Name
│ └─ DataRow
│ ├─ 1
│ └─ 山田太郎
│
└─ DataTable: Orders
├─ DataColumn: OrderId
├─ DataColumn: CustomerId
├─ DataColumn: Amount
└─ DataRow
├─ 101
├─ 1
└─ 5000
C#では、それぞれの要素へコレクションを通してアクセスします。
C#DataTable table = dataSet.Tables["Customers"]!;
DataColumn column = table.Columns["Name"]!;
DataRow row = table.Rows[0];
Console.WriteLine(row[column]);
テーブル名や列名を文字列で指定できるため、取得時には存在確認を行うと安全です。
1-5. DataSetを使うメリットとデメリット
DataSetの主なメリットは次のとおりです。
複数のDataTableをまとめて管理できる
テーブル間にDataRelationを設定できる
主キーや外部キーに相当する制約を設定できる
データベースとの接続を閉じた後もデータを編集できる
追加、変更、削除の状態をDataRowごとに管理できる
XMLとの相互変換機能が用意されている
Windows Formsなどの画面部品とデータバインディングしやすい
一方、次のようなデメリットもあります。
取得したデータをメモリ上に保持するため、大量データではメモリ消費が増える
列名を文字列で指定するコードでは、コンパイル時に入力ミスを検出しにくい
独自のエンティティクラスやEntity Framework Coreより型安全性が低い
小さな処理でもDataTableやDataColumnの定義が必要になる場合がある
データベース側の最新状態と自動同期されない
複数テーブル、リレーション、変更管理が必要な場面では便利ですが、単純な一覧取得だけでDataSetを使うと、構成が過剰になることがあります。
2. DataSetとDataTableの違い
2-1. DataSetは複数テーブル、DataTableは1つのテーブルを扱う
DataSetとDataTableの最大の違いは、管理する範囲です。
DataTableは1つの表形式データを管理します。対してDataSetは、複数のDataTableと、それらの関連をまとめて管理します。
顧客一覧だけを扱う場合は、DataTableだけでも十分です。
C#DataTable customers = new DataTable("Customers");
顧客と注文の両方を扱う場合は、DataSetへまとめると管理しやすくなります。
C#DataSet shopDataSet = new DataSet("ShopData");
shopDataSet.Tables.Add(customers);
shopDataSet.Tables.Add(orders);
DataSetの中にDataTableを1つだけ格納することもできますが、常にDataSetが必要なわけではありません。
2-2. DataSetとDataTableの構造を図で理解する
DataTableだけを使う場合の構造は次のとおりです。
DataTable: Customers
├─ CustomerId
├─ Name
DataSetを使う場合は、複数のDataTableを1つの単位として扱えます。
DataSet: ShopData
├─ DataTable: Customers
│ ├─ CustomerId
│ ├─ Name
│
├─ DataTable: Orders
│ ├─ OrderId
│ ├─ CustomerId
│ └─ Amount
│
└─ DataRelation: CustomerOrders
└─ Customers.CustomerId → Orders.CustomerId
DataSetにはRelationsコレクションがあり、親テーブルと子テーブルの関連を保持できます。
2-3. DataSetとDataTableの機能比較
| 比較項目 | DataSet | DataTable |
|---|---|---|
| 管理対象 | 複数テーブル | 1テーブル |
| 行・列の保持 | DataTableを通じて保持 | 直接保持 |
| テーブル間の関連 | DataRelationで管理可能 | 単独では管理しない |
| XML入出力 | 可能 | 可能 |
| 変更管理 | 複数テーブルをまとめて管理 | 1テーブル内で管理 |
| 主な用途 | 複数テーブルを扱う業務処理 | 一覧や単一テーブルの処理 |
| メモリ使用量 | 構成に応じて大きくなる | DataSetより小さくしやすい |
DataTable単体でも、行の追加、更新、削除、検索、並べ替えなどを実行できます。DataSetを使わなければできない代表的な処理は、複数テーブルとその関連を1つのまとまりとして管理することです。
2-4. DataSetを使うべきケース
次のようなケースでは、DataSetが適しています。
顧客と注文など、関連する複数テーブルをまとめて取得する
複数テーブルを編集してから一括で保存する
DataRelationを使って親子データをたどる
データベース接続を閉じた状態でデータを長時間編集する
既存のWindows Formsや.NET Frameworkシステムを保守する
XML形式で複数テーブルの構造とデータを保存する
実行時に列構成が決まる動的な表データを扱う
帳票や管理画面など、複数の表を一度に扱う処理でもDataSetが役立ちます。
2-5. DataTableだけで十分なケース
次のような処理では、DataTableだけで十分なことがあります。
1つの検索結果を画面へ表示する
CSVデータを一時的に表形式で保持する
1つのテーブルだけを並べ替え、絞り込みする
DataGridViewへ単一の一覧を表示する
テーブル間の関連を扱わない
さらに、列構成が固定されていて型安全性を重視する場合は、DataTableではなくList<T>や独自クラスを使う方法も検討できます。
3. C#でDataSetを作成する基本的な使い方
3-1. System.Data名前空間を読み込む
DataSetやDataTableは、System.Data名前空間に含まれています。
C#using System;
using System.Data;
現在の.NETでもDataSetやDataTableを使用できます。SQL Serverへ接続するクラスについては、後述するMicrosoft.Data.SqlClientを使用します。
3-2. DataSetのインスタンスを作成する
DataSetはnew演算子で作成します。
C#DataSet dataSet = new DataSet();
管理しやすいように名前を指定することもできます。
C#DataSet dataSet = new DataSet("ShopData");
DataSetの名前は、DataSetNameプロパティから取得できます。
C#Console.WriteLine(dataSet.DataSetName);
3-3. DataTableとDataColumnを作成する
次に、顧客情報を保持するDataTableを作成します。
C#DataTable customers = new DataTable("Customers");
DataColumn customerIdColumn =
customers.Columns.Add("CustomerId", typeof(int));
DataColumn nameColumn =
customers.Columns.Add("Name", typeof(string));
DataColumn emailColumn =
customers.Columns.Add("Email", typeof(string));
Columns.Addの第2引数では、列に格納する.NETの型を指定します。
主キーを設定する場合は、PrimaryKeyプロパティへDataColumnの配列を設定します。
C#customers.PrimaryKey = new[] { customerIdColumn };
列の制約も設定できます。
C#customerIdColumn.AllowDBNull = false;
nameColumn.AllowDBNull = false;
nameColumn.MaxLength = 100;
emailColumn.MaxLength = 255;
3-4. DataRowを追加してデータを登録する
DataRowを新しく作成するには、NewRowメソッドを使用します。
C#DataRow row = customers.NewRow();
row["CustomerId"] = 1;
row["Name"] = "山田太郎";
row["Email"] = "taro@example.com";
customers.Rows.Add(row);
短く記述する場合は、Rows.Addへ値を直接渡せます。
C#customers.Rows.Add(2, "佐藤花子", "hanako@example.com");
customers.Rows.Add(3, "鈴木一郎", DBNull.Value);
値を直接渡す場合は、DataColumnを追加した順番と値の順番を一致させる必要があります。
3-5. DataSetにDataTableを追加する
作成したDataTableは、DataSetのTablesコレクションへ追加します。
C#dataSet.Tables.Add(customers);
同じ名前のテーブルを重複して追加すると例外が発生します。追加前に確認する場合は、Containsメソッドを使います。
C#if (!dataSet.Tables.Contains("Customers"))
{
dataSet.Tables.Add(customers);
}
3-6. DataSetからテーブル・行・列の値を取得する
DataSetからDataTableを取得する方法は2つあります。
C#DataTable firstTable = dataSet.Tables[0];
DataTable customerTable = dataSet.Tables["Customers"]!;
行と列の値は、インデックスまたは列名で取得できます。
C#DataRow firstRow = customerTable.Rows[0];
int customerId = (int)firstRow["CustomerId"];
string customerName = (string)firstRow["Name"];
Console.WriteLine($"{customerId}: {customerName}");
列が存在しない可能性がある場合は、事前に確認します。
C#if (customerTable.Columns.Contains("Email"))
{
Console.WriteLine(firstRow["Email"]);
}
3-7. foreach文でDataSetのデータを一覧表示する
DataTableのすべての行を処理するには、foreach文を使用します。
C#foreach (DataRow customer in customerTable.Rows)
{
int id = Convert.ToInt32(customer["CustomerId"]);
string name = Convert.ToString(customer["Name"]) ?? "";
string email = customer.IsNull("Email")
? "未登録"
: Convert.ToString(customer["Email"]) ?? "";
Console.WriteLine($"{id}, {name}, {email}");
}
DataSetに含まれるすべてのテーブルを一覧表示することもできます。
C#foreach (DataTable table in dataSet.Tables)
{
Console.WriteLine($"テーブル名: {table.TableName}");
foreach (DataRow dataRow in table.Rows)
{
foreach (DataColumn column in table.Columns)
{
Console.Write($"{column.ColumnName}={dataRow[column]} ");
}
Console.WriteLine();
}
}
4. DataSetのデータを検索・編集・削除する方法
4-1. Rows.Findで主キーから行を検索する
主キーを使って行を検索する場合は、Rows.Findを使用します。
C#DataRow? foundRow = customerTable.Rows.Find(2);
if (foundRow != null)
{
Console.WriteLine(foundRow["Name"]);
}
Rows.Findを使用するには、DataTableのPrimaryKeyが設定されている必要があります。
複合主キーの場合は、検索値を配列で渡します。
C#DataRow? row = table.Rows.Find(new object[] { key1, key2 });
主キー検索を頻繁に行う処理では、行を順番に走査するよりもRows.Findが適しています。
4-2. DataTable.Selectで条件に一致する行を検索する
主キー以外の条件で検索する場合は、DataTable.Selectを使用できます。
C#DataRow[] selectedRows =
customerTable.Select("Name LIKE '%田%'");
foreach (DataRow selectedRow in selectedRows)
{
Console.WriteLine(selectedRow["Name"]);
}
複数条件も指定できます。
C#DataRow[] rows =
customerTable.Select(
"CustomerId >= 2 AND Email IS NOT NULL",
"CustomerId DESC");
第2引数には並べ替え条件を指定できます。Selectは、条件に一致するDataRowの配列を返します。Microsoft Learn
検索条件へ外部入力をそのまま連結すると、引用符などによって式が壊れることがあります。DataTableの検索式はSQLではありませんが、文字列を組み立てる場合は特殊文字の扱いに注意が必要です。
4-3. DataViewでデータを絞り込み・並べ替えする
DataViewを使うと、元のDataTableを変更せずに表示対象を絞り込んだり、並べ替えたりできます。
C#DataView view = new DataView(customerTable)
{
RowFilter = "Name LIKE '%田%'",
Sort = "CustomerId DESC"
};
foreach (DataRowView item in view)
{
Console.WriteLine(
$"{item["CustomerId"]}: {item["Name"]}");
}
DataViewは、Windows FormsのDataGridViewなどへデータをバインドするときにも利用できます。
C#dataGridView1.DataSource = view;
検索条件を変更するだけで表示内容を切り替えられるため、画面上の検索機能と相性がよいクラスです。
4-4. DataRowの値を更新する
DataRowの値は、列名またはDataColumnを指定して変更します。
C#DataRow? customer = customerTable.Rows.Find(1);
if (customer != null)
{
customer["Name"] = "山田次郎";
customer["Email"] = "jiro@example.com";
}
既存行を更新すると、通常はRowStateがModifiedになります。
C#Console.WriteLine(customer?.RowState);
RowStateには、主に次の値があります。
Detached:まだDataTableに追加されていないAdded:追加された行Unchanged:変更されていない行Modified:更新された行Deleted:削除予定の行
この変更状態は、後からデータベースへ更新内容を反映するときに使われます。
4-5. DataRowを削除する
DataRowを削除する基本的な方法は、Deleteメソッドです。
C#DataRow? customer = customerTable.Rows.Find(3);
if (customer != null)
{
customer.Delete();
}
Deleteを呼び出した直後は、既存行がDataTableから即座に消えるとは限りません。行は通常、Deleted状態として管理されます。
一方、Rows.RemoveはDataRowをコレクションから直接取り除きます。
C#customerTable.Rows.Remove(customer);
データベースへ削除を反映する予定がある場合は、変更状態を残せるDeleteを使うのが基本です。Deleteで削除予定にした行は、AcceptChangesが実行された時点でDataTableから取り除かれます。Microsoft Learn
4-6. AcceptChangesとRejectChangesで変更を確定・取り消しする
AcceptChangesは、DataSetやDataTable上の変更を確定します。
C#dataSet.AcceptChanges();
AcceptChangesを実行すると、追加行と更新行のRowStateはUnchangedになり、削除行はコレクションから取り除かれます。
変更を取り消す場合は、RejectChangesを使用します。
C#dataSet.RejectChanges();
RejectChangesを実行すると、一般的に次のように処理されます。
Addedの行:削除される
Modifiedの行:変更前の値へ戻る
Deletedの行:削除前の状態へ戻る
注意点は、SqlDataAdapter.Updateを実行する前にAcceptChangesを呼び出さないことです。AcceptChangesによって行がUnchangedになると、DataAdapterはその行を更新対象として認識できません。Microsoft Learn+1
4-7. DBNullを安全に判定する
データベースのNULLは、C#のnullではなくDBNull.ValueとしてDataTableに格納されることがあります。
次のコードは、DBNullを文字列へ直接変換しようとして例外になる可能性があります。
C#string email = (string)row["Email"];
安全に判定するには、IsNullメソッドを使用します。
C#string email;
if (row.IsNull("Email"))
{
email = "未登録";
}
else
{
email = Convert.ToString(row["Email"]) ?? "";
}
値を登録するときも、データベース上のNULLを表現する場合はDBNull.Valueを設定します。
C#row["Email"] = DBNull.Value;
nullはオブジェクト参照が存在しないことを示し、DBNull.Valueはデータベース上のNULL値を示す点が異なります。
5. DataSetで複数のDataTableを関連付ける方法
5-1. DataRelationとは
DataRelationは、DataSet内の親テーブルと子テーブルを関連付けるクラスです。
たとえば、次の関連を表現できます。
Customers.CustomerId
↓
Orders.CustomerId
DataRelationを設定すると、顧客行からその顧客の注文行を取得したり、注文行から顧客行を取得したりできます。
5-2. 親テーブルと子テーブルを用意する
まず、親となるCustomersテーブルを作成します。
C#DataTable customers = new DataTable("Customers");
DataColumn customerId =
customers.Columns.Add("CustomerId", typeof(int));
customers.Columns.Add("Name", typeof(string));
customers.PrimaryKey = new[] { customerId };
customers.Rows.Add(1, "山田太郎");
customers.Rows.Add(2, "佐藤花子");
続いて、子となるOrdersテーブルを作成します。
C#DataTable orders = new DataTable("Orders");
DataColumn orderId =
orders.Columns.Add("OrderId", typeof(int));
DataColumn orderCustomerId =
orders.Columns.Add("CustomerId", typeof(int));
orders.Columns.Add("OrderDate", typeof(DateTime));
orders.Columns.Add("Amount", typeof(decimal));
orders.PrimaryKey = new[] { orderId };
orders.Rows.Add(101, 1, new DateTime(2026, 6, 1), 5000m);
orders.Rows.Add(102, 1, new DateTime(2026, 6, 5), 8000m);
orders.Rows.Add(103, 2, new DateTime(2026, 6, 10), 3000m);
5-3. 主キーと外部キーに相当する列を設定する
親側のCustomers.CustomerIdには、重複しない値が必要です。
C#customers.PrimaryKey = new[] { customerId };
子側のOrders.CustomerIdには、親側に存在する顧客IDを格納します。
Customers
CustomerId=1
Orders
CustomerId=1
CustomerId=1
子側の列自体は重複して構いません。1人の顧客が複数の注文を持てるためです。
5-4. DataRelationをDataSetに追加する
2つのDataTableをDataSetへ追加し、DataRelationを設定します。
C#DataSet dataSet = new DataSet("ShopData");
dataSet.Tables.Add(customers);
dataSet.Tables.Add(orders);
DataRelation relation = new DataRelation(
"CustomerOrders",
customerId,
orderCustomerId,
createConstraints: true);
dataSet.Relations.Add(relation);
第4引数のcreateConstraintsをtrueにすると、関連に必要な制約も作成されます。
5-5. GetChildRowsで親行から子行を取得する
顧客行から注文行を取得するには、GetChildRowsを使用します。
C#DataRow? customer = customers.Rows.Find(1);
if (customer != null)
{
DataRow[] customerOrders =
customer.GetChildRows("CustomerOrders");
foreach (DataRow order in customerOrders)
{
Console.WriteLine(
$"注文番号: {order["OrderId"]}, " +
$"金額: {order["Amount"]}");
}
}
関連名の代わりにDataRelationオブジェクトを渡すこともできます。
C#DataRow[] childRows = customer!.GetChildRows(relation);
5-6. GetParentRowで子行から親行を取得する
注文行から顧客行を取得する場合は、GetParentRowを使用します。
C#DataRow orderRow = orders.Rows.Find(101)!;
DataRow customerRow =
orderRow.GetParentRow("CustomerOrders");
Console.WriteLine(
$"注文者: {customerRow["Name"]}");
SQLのJOINを実行しなくても、DataSet上で親子関係をたどれる点がDataRelationの特徴です。
5-7. Constraintでデータの整合性を保つ
DataSetやDataTableでは、主に次の制約を利用できます。
UniqueConstraint:値の重複を禁止するForeignKeyConstraint:親子関係の整合性を保つAllowDBNull:NULLを許可するか設定するMaxLength:文字列の最大長を設定する
DataRelation作成時に制約を有効にしている場合、存在しない顧客IDをOrdersへ追加すると例外が発生します。
C#// CustomerId=999の顧客が存在しない場合は制約違反
orders.Rows.Add(
104,
999,
new DateTime(2026, 6, 15),
1000m);
制約を一時的に無効化することもできます。
C#dataSet.EnforceConstraints = false;
// データ読み込み処理
dataSet.EnforceConstraints = true;
ただし、再びtrueにした時点で整合性が検証されます。制約違反が残っていると例外が発生するため、安易に無効化しないようにしましょう。
6. DataSetとデータベースを連携する方法
6-1. DataSetとSqlConnection・SqlDataAdapterの関係
SQL ServerとDataSetを連携する場合は、主に次のクラスを使用します。
SqlConnection:SQL Serverへの接続を管理するSqlCommand:実行するSQLやパラメーターを管理するSqlDataAdapter:データベースとDataSetの橋渡しをするDataSet:取得したデータをメモリ上で管理する
SqlDataAdapterは、Fillで検索結果をDataSetへ読み込み、UpdateでDataSet上の変更をデータベースへ反映します。Microsoft Learn+2
Microsoft Learn+2
6-2. SQL Serverへ接続するための準備
現在の.NETプロジェクトでSQL Serverへ接続する場合は、NuGetからMicrosoft.Data.SqlClientパッケージを追加します。
Bashdotnet add package Microsoft.Data.SqlClient
C#ファイルでは、次の名前空間を読み込みます。
C#using System;
using System.Data;
using Microsoft.Data.SqlClient;
Microsoft.Data.SqlClientは、従来のSystem.Data.SqlClientから独立して提供されているSQL Server用データプロバイダーです。既存システムではSystem.Data.SqlClientが使われている場合もありますが、新しい.NETプロジェクトではMicrosoft.Data.SqlClientを選択できます。Microsoft Learn+1
接続文字列は、実際の環境に合わせて設定します。
C#string connectionString =
"Server=localhost;" +
"Database=SampleDb;" +
"Integrated Security=True;" +
"TrustServerCertificate=True;";
パスワードなどの秘密情報をソースコードへ直接記述せず、設定ファイル、環境変数、シークレット管理サービスなどで管理することが重要です。
6-3. SqlDataAdapter.FillでSELECT結果をDataSetへ格納する
次の例では、Customersテーブルの検索結果をDataSetへ読み込みます。
C#string connectionString =
"Server=localhost;" +
"Database=SampleDb;" +
"Integrated Security=True;" +
"TrustServerCertificate=True;";
const string sql = """
SELECT CustomerId, Name, Email
FROM dbo.Customers
ORDER BY CustomerId;
""";
DataSet dataSet = new DataSet();
using SqlConnection connection =
new SqlConnection(connectionString);
using SqlDataAdapter adapter =
new SqlDataAdapter(sql, connection);
adapter.Fill(dataSet, "Customers");
Fillは、指定したDataTableが存在しない場合、検索結果の列情報を使ってテーブルを作成します。既に同名のテーブルが存在する場合は、そのスキーマを使用してデータを格納します。Microsoft Learn+1
6-4. DataSetに格納したデータを取得・表示する
Fillしたデータは、通常のDataTableと同じ方法で取得できます。
C#DataTable customers =
dataSet.Tables["Customers"]
?? throw new InvalidOperationException(
"Customersテーブルがありません。");
foreach (DataRow row in customers.Rows)
{
int id = Convert.ToInt32(row["CustomerId"]);
string name = Convert.ToString(row["Name"]) ?? "";
string email = row.IsNull("Email")
? "未登録"
: Convert.ToString(row["Email"]) ?? "";
Console.WriteLine($"{id}: {name}, {email}");
}
テーブル名を指定せずにFillした場合は、Tableなどの自動生成名になることがあります。後から分かりやすく参照できるよう、明示的なテーブル名を指定するのがおすすめです。
6-5. 複数のSELECT結果を複数のDataTableへ格納する
1つのSQLで複数のSELECT文を実行すると、複数の結果セットをDataSetへ格納できます。
C#const string sql = """
SELECT CustomerId, Name, Email
FROM dbo.Customers;
SELECT OrderId, CustomerId, OrderDate, Amount
FROM dbo.Orders;
""";
DataSet dataSet = new DataSet();
using SqlConnection connection =
new SqlConnection(connectionString);
using SqlDataAdapter adapter =
new SqlDataAdapter(sql, connection);
adapter.TableMappings.Add("Table", "Customers");
adapter.TableMappings.Add("Table1", "Orders");
adapter.Fill(dataSet);
これにより、次の名前で取得できます。
C#DataTable customers = dataSet.Tables["Customers"]!;
DataTable orders = dataSet.Tables["Orders"]!;
別々のSqlDataAdapterで同じDataSetへFillする方法もあります。
C#customerAdapter.Fill(dataSet, "Customers");
orderAdapter.Fill(dataSet, "Orders");
SQLの責務や検索条件が異なる場合は、処理を分けたほうが保守しやすいことがあります。
6-6. パラメーター付きSQLで安全にデータを検索する
利用者が入力した値をSQL文字列へ直接連結してはいけません。
C#// 避けるべき例
string sql =
"SELECT * FROM dbo.Customers " +
"WHERE Name = '" + inputName + "'";
パラメーター付きSQLを使用します。
C#const string sql = """
SELECT CustomerId, Name, Email
FROM dbo.Customers
WHERE Name LIKE @keyword
ORDER BY CustomerId;
""";
string keyword = "山田";
using SqlConnection connection =
new SqlConnection(connectionString);
using SqlDataAdapter adapter =
new SqlDataAdapter(sql, connection);
adapter.SelectCommand.Parameters
.Add("@keyword", SqlDbType.NVarChar, 100)
.Value = $"%{keyword}%";
DataSet dataSet = new DataSet();
adapter.Fill(dataSet, "Customers");
パラメーターを使用すると、入力値がSQLの構文ではなくデータとして扱われます。また、型や最大長も明示できます。
6-7. using文でデータベース接続を適切に破棄する
SqlConnection、SqlCommand、SqlDataAdapterなどは、不要になったら適切に破棄する必要があります。
C#using SqlConnection connection =
new SqlConnection(connectionString);
using SqlCommand command =
new SqlCommand(sql, connection);
connection.Open();
// SQLを実行する
usingブロックを抜けると、例外が発生した場合でもDisposeが呼び出されます。
DataSetはデータベース接続とは独立しているため、接続を破棄した後もDataSet内のデータを利用できます。
7. DataSetの変更をデータベースへ反映する方法
7-1. DataSet上のデータを追加・更新・削除する
まず、SqlDataAdapterでデータを読み込みます。
C#const string selectSql = """
SELECT CustomerId, Name, Email
FROM dbo.Customers;
""";
DataSet dataSet = new DataSet();
using SqlConnection connection =
new SqlConnection(connectionString);
using SqlDataAdapter adapter =
new SqlDataAdapter(selectSql, connection);
adapter.MissingSchemaAction =
MissingSchemaAction.AddWithKey;
adapter.Fill(dataSet, "Customers");
DataTable customers =
dataSet.Tables["Customers"]!;
その後、DataTable上で変更します。
C#// 追加
customers.Rows.Add(
10,
"高橋美咲",
"misaki@example.com");
// 更新
DataRow? updateRow = customers.Rows.Find(1);
if (updateRow != null)
{
updateRow["Name"] = "山田次郎";
}
// 削除
DataRow? deleteRow = customers.Rows.Find(3);
deleteRow?.Delete();
この時点では、変更はメモリ上にあるだけで、データベースへは反映されていません。
7-2. SqlDataAdapter.Updateの仕組み
SqlDataAdapter.Updateは、各DataRowのRowStateを確認し、状態に対応するコマンドを実行します。
Added:InsertCommandを実行
Modified:UpdateCommandを実行
Deleted:DeleteCommandを実行
Unchanged:何もしない
C#int affectedRows =
adapter.Update(dataSet, "Customers");
Console.WriteLine(
$"{affectedRows}件を更新しました。");
Updateは、DataSet内の変更を自動的にSQLへ変換する機能ではありません。基本的にはInsertCommand、UpdateCommand、DeleteCommandの設定が必要です。Microsoft Learn+1
7-3. InsertCommand・UpdateCommand・DeleteCommandを設定する
更新用コマンドを手動で設定する例は次のとおりです。
C#adapter.InsertCommand = new SqlCommand(
"""
INSERT INTO dbo.Customers
(CustomerId, Name, Email)
VALUES
(@CustomerId, @Name, @Email);
""",
connection);
adapter.InsertCommand.Parameters.Add(
"@CustomerId",
SqlDbType.Int,
0,
"CustomerId");
adapter.InsertCommand.Parameters.Add(
"@Name",
SqlDbType.NVarChar,
100,
"Name");
adapter.InsertCommand.Parameters.Add(
"@Email",
SqlDbType.NVarChar,
255,
"Email");
UpdateCommandも設定します。
C#adapter.UpdateCommand = new SqlCommand(
"""
UPDATE dbo.Customers
SET
Name = @Name,
Email = @Email
WHERE
CustomerId = @OriginalCustomerId;
""",
connection);
adapter.UpdateCommand.Parameters.Add(
"@Name",
SqlDbType.NVarChar,
100,
"Name");
adapter.UpdateCommand.Parameters.Add(
"@Email",
SqlDbType.NVarChar,
255,
"Email");
SqlParameter updateId =
adapter.UpdateCommand.Parameters.Add(
"@OriginalCustomerId",
SqlDbType.Int,
0,
"CustomerId");
updateId.SourceVersion =
DataRowVersion.Original;
DeleteCommandも同様です。
C#adapter.DeleteCommand = new SqlCommand(
"""
DELETE FROM dbo.Customers
WHERE CustomerId = @OriginalCustomerId;
""",
connection);
SqlParameter deleteId =
adapter.DeleteCommand.Parameters.Add(
"@OriginalCustomerId",
SqlDbType.Int,
0,
"CustomerId");
deleteId.SourceVersion =
DataRowVersion.Original;
SourceColumnにはDataTableの列名を指定します。SourceVersionには、現在値と変更前の値のどちらを使うかを指定します。
主キーが変更される可能性を考慮すると、WHERE句ではDataRowVersion.Originalの値を使うのが基本です。
7-4. SqlCommandBuilderで更新用コマンドを自動生成する
単純な単一テーブルへの更新であれば、SqlCommandBuilderを使用して更新用コマンドを自動生成できます。
C#const string sql = """
SELECT CustomerId, Name, Email
FROM dbo.Customers;
""";
using SqlConnection connection =
new SqlConnection(connectionString);
using SqlDataAdapter adapter =
new SqlDataAdapter(sql, connection);
adapter.MissingSchemaAction =
MissingSchemaAction.AddWithKey;
using SqlCommandBuilder builder =
new SqlCommandBuilder(adapter);
DataSet dataSet = new DataSet();
adapter.Fill(dataSet, "Customers");
DataTable customers =
dataSet.Tables["Customers"]!;
DataRow? row = customers.Rows.Find(1);
if (row != null)
{
row["Name"] = "変更後の名前";
}
adapter.Update(dataSet, "Customers");
SqlCommandBuilderは、SelectCommandを基に単一テーブル用のINSERT、UPDATE、DELETEコマンドを生成します。JOINを含む複雑なSELECTや、主キーを特定できない検索では、期待どおりに生成できないことがあります。Microsoft Learn+1
業務ルールが複雑な処理では、SQLを明示的に設定するか、ストアドプロシージャを利用したほうが安全です。
7-5. Updateで変更内容をデータベースへ保存する
更新用コマンドを設定した後、Updateを呼び出します。
C#if (dataSet.HasChanges())
{
int affectedRows =
adapter.Update(dataSet, "Customers");
Console.WriteLine(
$"{affectedRows}行を保存しました。");
}
通常、正常に更新された行に対しては変更が受け入れられ、RowStateがUnchangedになります。
変更内容だけを取得することもできます。
C#DataSet? changes = dataSet.GetChanges();
if (changes != null)
{
adapter.Update(changes, "Customers");
}
ただし、GetChangesで作成した別のDataSetを更新した場合、元のDataSetとの状態統合を考慮する必要があります。単純な処理では元のDataSetをそのままUpdateしたほうが分かりやすいでしょう。
7-6. トランザクションを使って安全に更新する
複数の更新をまとめて成功または失敗させる場合は、トランザクションを使用します。
C#connection.Open();
using SqlTransaction transaction =
connection.BeginTransaction();
adapter.InsertCommand!.Transaction = transaction;
adapter.UpdateCommand!.Transaction = transaction;
adapter.DeleteCommand!.Transaction = transaction;
try
{
adapter.Update(dataSet, "Customers");
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
複数テーブルを更新する場合も、同じ接続とトランザクションを各コマンドへ設定します。
C#try
{
customerAdapter.Update(
dataSet,
"Customers");
orderAdapter.Update(
dataSet,
"Orders");
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
外部キーがある場合は、更新順序にも注意します。
追加:親テーブルを先に追加する
削除:子テーブルを先に削除する
7-7. 同時実行制御エラーが発生した場合の対処法
DataSetへデータを読み込んだ後、保存するまでの間に別の利用者が同じ行を更新することがあります。
UpdateCommandやDeleteCommandの対象行が見つからず、影響行数が0になると、DBConcurrencyExceptionが発生する場合があります。
C#try
{
adapter.Update(dataSet, "Customers");
}
catch (DBConcurrencyException ex)
{
Console.WriteLine(
"ほかの利用者によってデータが変更された可能性があります。");
Console.WriteLine(ex.Message);
}
主な対処方法は次のとおりです。
データベースから最新データを再取得する
利用者に変更内容を確認してもらう
更新前の値をWHERE句へ含める
SQL Serverの
rowversion列を使って変更を検知する無条件に上書きするか、競合として扱うかを業務要件に沿って決める
楽観的同時実行制御では、取得時の値と保存時の値が一致していることを確認してから更新します。競合時に単純な再試行を行うと、別の利用者の変更を上書きする可能性があるため注意が必要です。Microsoft Learn+2
Microsoft Learn+2
8. DataSetとXMLを相互変換する方法
8-1. WriteXmlでDataSetをXMLファイルへ保存する
DataSetの内容は、WriteXmlメソッドでXMLファイルへ保存できます。
C#dataSet.WriteXml("shop-data.xml");
データとスキーマを同じXMLへ含める場合は、XmlWriteMode.WriteSchemaを指定します。
C#dataSet.WriteXml(
"shop-data.xml",
XmlWriteMode.WriteSchema);
DataSetは、データのみ、またはデータとスキーマの両方をXMLへ出力できます。Microsoft Learn+1
8-2. ReadXmlでXMLファイルからデータを読み込む
保存したXMLは、ReadXmlで読み込めます。
C#DataSet loadedDataSet =
new DataSet();
loadedDataSet.ReadXml("shop-data.xml");
読み込み後は、通常のDataSetと同様に操作できます。
C#foreach (DataRow row
in loadedDataSet.Tables["Customers"]!.Rows)
{
Console.WriteLine(row["Name"]);
}
XML内にスキーマが含まれていない場合、DataSetが構造を推測することがあります。列の型を正確に維持したい場合は、スキーマも保存しておくと安全です。Microsoft Learn
8-3. WriteXmlSchemaでスキーマを保存する
データを含めず、テーブルや列、制約などのスキーマだけを保存するには、WriteXmlSchemaを使用します。
C#dataSet.WriteXmlSchema(
"shop-schema.xsd");
読み込む場合は、ReadXmlSchemaを使用します。
C#DataSet schemaDataSet =
new DataSet();
schemaDataSet.ReadXmlSchema(
"shop-schema.xsd");
その後、データだけが入ったXMLを読み込むこともできます。
C#schemaDataSet.ReadXml(
"shop-data.xml",
XmlReadMode.IgnoreSchema);
8-4. CopyとCloneの違い
DataSetの複製には、CopyとCloneがあります。
Copyはスキーマとデータの両方を複製します。
C#DataSet copiedDataSet =
dataSet.Copy();
Cloneはスキーマ、リレーション、制約を複製しますが、行データは複製しません。
C#DataSet emptyDataSet =
dataSet.Clone();
使い分けは次のとおりです。
| メソッド | スキーマ | リレーション・制約 | 行データ |
|---|---|---|---|
| Copy | コピーする | コピーする | コピーする |
| Clone | コピーする | コピーする | コピーしない |
同じ形式の空のDataSetを作りたい場合はClone、データを含むバックアップをメモリ上に作りたい場合はCopyが適しています。Microsoft Learn+1
8-5. XML連携を利用する際の注意点
DataSetとXMLを連携するときは、次の点に注意してください。
まず、XMLへ出力するとファイルサイズが大きくなることがあります。大量データの保存形式として適しているとは限りません。
次に、スキーマがないXMLを読み込むと、列の型が意図と異なる形で推測される可能性があります。型を維持したい場合は、WriteSchemaまたはXSDを利用します。
また、信頼できないXMLを無制限に読み込まないことも重要です。ファイルサイズ、要素数、スキーマなどを検証し、アプリケーションが想定する形式だけを受け入れるようにします。
機密情報を含むDataSetをXMLへ保存する場合は、保存場所のアクセス権や暗号化も検討してください。
9. DataSetを使う際によくあるエラーと対処法
9-1. Tablesの指定で「テーブルが存在しない」と表示される
次のコードでnullが返ることがあります。
C#DataTable? table =
dataSet.Tables["Customers"];
主な原因は次のとおりです。
Fill時にテーブル名を指定していない
テーブル名の大文字・小文字やスペルが異なる
Fillが実行されていない
別のDataSetを参照している
TableMappingsの指定が間違っている
まず、実際に格納されているテーブル名を確認します。
C#foreach (DataTable item in dataSet.Tables)
{
Console.WriteLine(item.TableName);
}
取得時は、存在しない場合を明示的に処理すると安全です。
C#DataTable customers =
dataSet.Tables["Customers"]
?? throw new InvalidOperationException(
"Customersテーブルが存在しません。");
9-2. 列名の指定ミスで例外が発生する
存在しない列名を指定すると、例外が発生します。
C#// Name列をNmaeと入力している
Console.WriteLine(row["Nmae"]);
取得前に確認できます。
C#if (table.Columns.Contains("Name"))
{
Console.WriteLine(row["Name"]);
}
列名を複数箇所で使う場合は、定数にまとめる方法もあります。
C#private const string NameColumn = "Name";
型付きDataSetや独自のエンティティクラスを使用すると、文字列による指定ミスを減らせます。
9-3. nullとDBNullを混同してエラーになる
DataRowの列へnullを設定すると、列の設定や型によっては例外になることがあります。
データベースのNULLを設定する場合はDBNull.Valueを使用します。
C#row["Email"] =
string.IsNullOrWhiteSpace(email)
? DBNull.Value
: email;
取得時は、row["Email"] == nullではなく、row.IsNull("Email")またはConvert.IsDBNullで確認します。
C#if (Convert.IsDBNull(row["Email"]))
{
Console.WriteLine("メールアドレス未登録");
}
9-4. Fillしても主キー情報が設定されない
通常のFillでは、列名と型が読み込まれても、主キー情報が自動設定されない場合があります。
主キー情報も取得したい場合は、Fillの前にMissingSchemaAction.AddWithKeyを指定します。
C#adapter.MissingSchemaAction =
MissingSchemaAction.AddWithKey;
adapter.Fill(dataSet, "Customers");
または、FillSchemaを使用します。
C#adapter.FillSchema(
dataSet,
SchemaType.Source,
"Customers");
adapter.Fill(dataSet, "Customers");
主キー情報がないと、Rows.Findを使用できず、同じデータを再Fillしたときに行が追加され続ける可能性があります。AddWithKeyやFillSchemaには追加のスキーマ取得処理が必要になるため、主キーが分かっている場合はコード上で明示する方法もあります。Microsoft Learn+2
Microsoft Learn+2
9-5. Updateしてもデータベースへ反映されない
Updateしても反映されない場合は、次の点を確認します。
InsertCommand、UpdateCommand、DeleteCommandが設定されているか
SqlCommandBuilderが正しく構成されているか
更新対象行のRowStateがModified、Added、Deletedになっているか
Update前にAcceptChangesを実行していないか
WHERE句が対象行と一致しているか
トランザクションをCommitしているか
Update対象のDataTable名が正しいか
例外を握りつぶしていないか
RowStateを確認すると原因を特定しやすくなります。
C#foreach (DataRow row in customers.Rows)
{
Console.WriteLine(row.RowState);
}
変更行だけを確認する場合は、GetChangesを使います。
C#DataTable? changes =
customers.GetChanges();
Console.WriteLine(
changes?.Rows.Count ?? 0);
9-6. 大量データの読み込みでメモリ使用量が増える
DataSetは取得した行、列、スキーマ、変更前の値、制約などをメモリ上で管理します。そのため、大量データを一括で読み込むとメモリ使用量が増加します。
対処方法は次のとおりです。
WHERE句で必要な行だけ取得する
SELECT句で必要な列だけ取得する
ページングを実装する
集計は可能な限りデータベース側で行う
読み取り専用の大量処理ではSqlDataReaderを使う
不要になったDataSetへの参照を保持し続けない
複数回Fillするときに重複追加されないか確認する
たとえば、全件取得ではなく範囲を指定します。
SQLSELECT CustomerId, Name, Email
FROM dbo.Customers
ORDER BY CustomerId
OFFSET @offset ROWS
FETCH NEXT @pageSize ROWS ONLY;
大量データを一度にクライアントへ転送する処理は、データベース、ネットワーク、アプリケーションメモリのすべてに負荷をかけます。Microsoft Learn
9-7. SQLインジェクションを防ぐための注意点
利用者が入力した値をSQL文字列へ連結すると、SQLインジェクションの原因になります。
必ずパラメーターを使用します。
C#const string sql = """
SELECT CustomerId, Name
FROM dbo.Customers
WHERE CustomerId = @customerId;
""";
using SqlCommand command =
new SqlCommand(sql, connection);
command.Parameters
.Add("@customerId", SqlDbType.Int)
.Value = customerId;
テーブル名や列名は通常のSQLパラメーターにできません。動的に切り替える必要がある場合は、許可済みの値だけを選択できるホワイトリスト方式にします。
C#string orderBy = sortKey switch
{
"name" => "Name",
"id" => "CustomerId",
_ => "CustomerId"
};
接続に使用するデータベースユーザーへ、必要以上の権限を与えないことも重要です。
10. DataSetとほかのデータ取得方法の使い分け
10-1. DataSetとSqlDataReaderの違い
SqlDataReaderは、SQL Serverから取得した行を前方向へ順番に読み取るクラスです。
C#using SqlConnection connection =
new SqlConnection(connectionString);
using SqlCommand command =
new SqlCommand(
"""
SELECT CustomerId, Name
FROM dbo.Customers;
""",
connection);
connection.Open();
using SqlDataReader reader =
command.ExecuteReader();
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
Console.WriteLine($"{id}: {name}");
}
DataReaderはデータをストリームとして順次処理するため、大量データを一度だけ読み取る処理に向いています。DataSetのように検索結果全体をキャッシュせず、前方向へ読み進めます。Microsoft Learn+2
Microsoft Learn+2
| 比較項目 | DataSet | SqlDataReader |
|---|---|---|
| 接続方式 | 非接続型 | 読み取り中は接続が必要 |
| データ保持 | メモリ上へ全体を保持 | 順次読み取り |
| 複数回の参照 | 可能 | 基本的に前方向のみ |
| 編集 | 可能 | 読み取り専用 |
| 複数テーブル | 管理しやすい | 複数結果の順次処理は可能 |
| 大量データ | メモリ負荷が高くなりやすい | 比較的適している |
| 主な用途 | 画面編集、複数テーブル管理 | 高速な一覧処理、バッチ |
10-2. DataSetとEntity Framework Coreの違い
Entity Framework Coreは、データベースの行をC#のクラスとして扱うORMです。
C#public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; } = "";
public string? Email { get; set; }
}
EF Coreでは、LINQを使って検索できます。
C#List<Customer> customers =
await dbContext.Customers
.Where(x => x.Name.Contains("山田"))
.ToListAsync();
主な違いは次のとおりです。
| 比較項目 | DataSet | Entity Framework Core |
|---|---|---|
| データ表現 | DataTable、DataRow | エンティティクラス |
| 型安全性 | 列名を文字列指定することが多い | プロパティとして参照 |
| 検索 | SQL、Select、DataViewなど | LINQ |
| 変更管理 | DataRowState | DbContextの変更追跡 |
| リレーション | DataRelation | ナビゲーションプロパティ |
| 動的な列構成 | 扱いやすい | 固定モデルが中心 |
| 既存システム | ADO.NET系で多い | 新しい.NET開発で採用しやすい |
新規の業務アプリケーションで、ドメインモデルや型安全性を重視する場合はEF Coreが有力です。一方、実行時に列構成が変わる帳票、複数の検索結果を表形式のまま扱う処理、既存ADO.NETシステムの改修ではDataSetが適することがあります。
10-3. DataSetとList・コレクションの違い
List<T>は、同じ型のオブジェクトをまとめて管理する汎用コレクションです。
C#List<Customer> customers = new()
{
new Customer
{
CustomerId = 1,
Name = "山田太郎"
},
new Customer
{
CustomerId = 2,
Name = "佐藤花子"
}
};
Listのメリットは、C#の型システムを利用できることです。
C#string name = customers[0].Name;
DataRowでは、列名の入力ミスが実行時まで分からないことがあります。
C#string name =
Convert.ToString(row["Name"]) ?? "";
一方、DataSetには次の機能があります。
DataTableによる表形式データ
主キーや制約
DataRelation
DataRowStateによる変更管理
XML入出力
DataAdapterとの連携
実行時に決まる列構成
単純なデータの一覧であればList、データベースに近い表構造や変更管理が必要であればDataSetを検討します。
10-4. 複数テーブルやリレーションを扱う場合の選び方
顧客、注文、注文明細のように複数テーブルを扱う場合でも、常にDataSetが最適とは限りません。
DataSetが適するのは、次のような場合です。
テーブル形式のままデータを扱いたい
DataRelationで親子行をたどりたい
列構成が実行時に決まる
既存のDataSetベースの処理へ追加する
Windows Formsのデータバインディングを利用する
EF Coreが適するのは、次のような場合です。
CustomerやOrderなどの業務モデルをクラスで表現したい
LINQで検索したい
型安全性を重視する
関連エンティティをナビゲーションプロパティで扱いたい
新規開発で保守性を重視する
Listや独自クラスが適するのは、データ量や構造が小さく、データベースとの自動的な変更同期が不要な場合です。
10-5. 大量データや高速処理が必要な場合の選び方
大量データを順番に処理するだけであれば、DataSetよりSqlDataReaderが適しています。
たとえば、数十万行を読み取ってファイルへ出力する処理では、すべての行をDataSetへ保持する必要がありません。
C#while (await reader.ReadAsync())
{
// 1行ずつ処理して出力する
}
画面に表示する場合も、全件を読み込むのではなく、ページングや検索条件を使って必要な範囲だけ取得します。
選択の目安は次のとおりです。
全件を1回だけ処理する:SqlDataReader
一部のデータを編集して保存する:DataSetまたはEF Core
複数回検索・並べ替えする:DataTable、DataView
型安全な業務ロジックを実装する:EF CoreまたはList
CSVやExcel向けに動的な表を作る:DataTable
大量データを一括コピーする:SqlBulkCopyなど専用API
「DataSetは遅い」「DataReaderなら必ず速い」と決めつけるのではなく、取得件数、ネットワーク転送量、メモリ使用量、処理方法を基に判断することが重要です。
10-6. 新規開発と既存システム改修での判断基準
新規開発では、次の観点で選びます。
データ構造をクラスとして定義できるか
テーブル構造が固定されているか
ORMを利用する方針か
複数テーブルの編集単位はどこか
大量データを扱うか
既存の画面部品や帳票ツールとの相性はどうか
一般的なCRUD中心の新規Webアプリケーションでは、EF CoreやDapperなどを採用する構成が考えられます。一方、動的な表データ、帳票、Windows Forms、DataGridView、既存ADO.NET資産との連携ではDataSetが実用的です。
既存システムの改修では、周囲の設計との一貫性も重要です。DataSetを一部分だけ別方式へ置き換えると、変換処理やテスト範囲が増えることがあります。
既存処理が安定しており、変更内容が小さい場合はDataSetを継続利用するほうが安全なことがあります。大規模な刷新で保守性や型安全性に課題がある場合は、段階的な移行を検討するとよいでしょう。
まとめ
C#のDataSetは、複数のDataTableをメモリ上で管理し、テーブル間の関連、制約、データの変更状態まで扱えるクラスです。
DataTableが1つの表を表すのに対し、DataSetは複数の表とその関連をまとめて保持します。データベースへの接続を閉じた後も検索や編集ができるため、ADO.NETでは非接続型のデータ管理に利用されます。
DataSetを使う際の重要なポイントは次のとおりです。
DataTable、DataRow、DataColumnの関係を理解する
Rows.Findを使う場合は主キーを設定する
NULLはnullではなくDBNull.Valueとして扱う
DataRelationで親子テーブルを関連付けられる
SqlDataAdapter.Fillでデータベースから読み込む
SqlDataAdapter.Updateで変更をデータベースへ反映する
Update前にAcceptChangesを実行しない
SQLにはパラメーターを使用する
大量データではDataReaderやページングを検討する
新規開発ではEF CoreやListとの使い分けを考える
DataSetはすべてのデータアクセス処理に必要なクラスではありません。しかし、複数テーブルを表形式のまま扱う処理、データベースと切り離した編集、既存ADO.NETシステムの保守、XML連携などでは、現在でも有効な選択肢です。

