C# DataSetとは?使い方・DataTableとの違い・DB連携をサンプルコードで初心者向けに解説

はじめに

C#でデータベースを扱う方法を調べると、DataSetDataTableSqlDataAdapterといったクラスがよく登場します。しかし、初心者にとっては「DataSetとDataTableは何が違うのか」「データベースに直接接続するクラスなのか」「現在の.NETでも使う場面があるのか」が分かりにくいかもしれません。

DataSetは、複数の表形式データをメモリ上に保持し、テーブル間の関連や制約、データの変更状態まで管理できるクラスです。ADO.NETでは、データベースなどのデータソースと切り離してデータを扱うための中心的な役割を持ちます。Microsoft Learn+2Microsoft 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つの要素が使われます。

  1. データベースへ接続してSQLを実行するデータプロバイダー

  2. 取得したデータをメモリ上で管理するDataSet

SQL Serverを使用する場合は、SqlConnectionSqlCommandSqlDataReaderSqlDataAdapterなどがデータプロバイダー側のクラスです。

DataSetはデータソースに直接依存しません。SQL Serverから取得したデータだけでなく、XMLから読み込んだデータや、プログラム内で作成したデータも同じ形式で管理できます。

1-3. DataSetが「非接続型」と呼ばれる理由

DataSetは、データベースとの接続を維持しなくてもデータを操作できるため、「非接続型」と呼ばれます。

一般的な処理の流れは次のとおりです。

  1. データベースへ接続する

  2. 必要なデータをDataSetへ読み込む

  3. データベース接続を閉じる

  4. DataSet上で検索や編集を行う

  5. 必要になった時点で再接続し、変更をデータベースへ反映する

データを取得した後も接続を開き続ける必要がないため、画面上で時間をかけて編集するデスクトップアプリケーションや、複数テーブルをまとめて扱う処理に適しています。

一方、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
└─ Email

DataSetを使う場合は、複数のDataTableを1つの単位として扱えます。

DataSet: ShopData
├─ DataTable: Customers
│ ├─ CustomerId
│ ├─ Name
│ └─ Email

├─ DataTable: Orders
│ ├─ OrderId
│ ├─ CustomerId
│ └─ Amount

└─ DataRelation: CustomerOrders
└─ Customers.CustomerId → Orders.CustomerId

DataSetにはRelationsコレクションがあり、親テーブルと子テーブルの関連を保持できます。

2-3. DataSetとDataTableの機能比較

比較項目DataSetDataTable
管理対象複数テーブル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";
}

既存行を更新すると、通常はRowStateModifiedになります。

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引数のcreateConstraintstrueにすると、関連に必要な制約も作成されます。

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+2Microsoft Learn+2

6-2. SQL Serverへ接続するための準備

現在の.NETプロジェクトでSQL Serverへ接続する場合は、NuGetからMicrosoft.Data.SqlClientパッケージを追加します。

Bash
dotnet 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+2Microsoft 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の複製には、CopyCloneがあります。

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+2Microsoft 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するときに重複追加されないか確認する

たとえば、全件取得ではなく範囲を指定します。

SQL
SELECT 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+2Microsoft Learn+2

比較項目DataSetSqlDataReader
接続方式非接続型読み取り中は接続が必要
データ保持メモリ上へ全体を保持順次読み取り
複数回の参照可能基本的に前方向のみ
編集可能読み取り専用
複数テーブル管理しやすい複数結果の順次処理は可能
大量データメモリ負荷が高くなりやすい比較的適している
主な用途画面編集、複数テーブル管理高速な一覧処理、バッチ

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();

主な違いは次のとおりです。

比較項目DataSetEntity Framework Core
データ表現DataTable、DataRowエンティティクラス
型安全性列名を文字列指定することが多いプロパティとして参照
検索SQL、Select、DataViewなどLINQ
変更管理DataRowStateDbContextの変更追跡
リレーション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連携などでは、現在でも有効な選択肢です。