C#を用いたアプリケーション開発において、メモリ上でテーブル形式のデータを管理するための強力なツールが DataTableクラス です。

かつてADO.NETの中心的役割を担っていたDataTableは、Entity FrameworkなどのORM(オブジェクト関係マッピング)が普及した現代においても、動的なデータ構造を扱う場面や、CSV・Excelデータのインポート、SQLクエリの結果を一時的に保持する用途などで幅広く活用されています。

本記事では、DataTableの基礎から応用までを網羅し、現場で役立つ実践的なテクニックを詳しく解説します。

DataTableの基本概念と準備

DataTableは、System.Data 名前空間に含まれるクラスであり、リレーショナルデータベースの「テーブル」をメモリ上に模した構造を持っています。

1つのDataTableは、列の定義を持つ DataColumnCollection (Columnsプロパティ)と、実際のデータ行を保持する DataRowCollection (Rowsプロパティ)で構成されます。

DataTableを利用するためには、まずプロジェクトの参照を確認し、ソースファイルの先頭に以下のディレクティブを記述する必要があります。

C#
using System;
using System.Data;
using System.Linq; // LINQを活用する場合に必要

DataTableは、単独で使用することも可能ですが、複数のDataTableをまとめて管理する DataSet の一部として利用されることも一般的です。

DataTableの初期化と列(DataColumn)の定義

DataTableを使用する際の最初のステップは、インスタンスの作成と スキーマ(構造)の定義 です。

列の名前やデータ型をあらかじめ定義することで、型安全性の向上やエラーの防止に繋がります。

DataTableのインスタンス化

DataTableを作成するには、コンストラクタを使用します。

引数にテーブル名を指定すると、後でDataSetに含める際などの識別子として利用できます。

C#
// テーブル名の指定なし
DataTable dt1 = new DataTable();

// テーブル名を指定して初期化
DataTable dt2 = new DataTable("EmployeeTable");

列(DataColumn)の追加

次に、テーブルにどのようなデータを入れるかを定義します。

Columns.Add メソッドを使用し、列名とデータ型を指定します。

C#
DataTable employeeTable = new DataTable("Employees");

// 列の追加(列名、データ型を指定)
employeeTable.Columns.Add("Id", typeof(int));
employeeTable.Columns.Add("Name", typeof(string));
employeeTable.Columns.Add("Department", typeof(string));
employeeTable.Columns.Add("JoinedDate", typeof(DateTime));

制約の設定

DataTableでは、データベースと同様に 主キー(Primary Key) や一意制約、Null許容否認などの制約を設定できます。

C#
// Id列を主キーに設定
employeeTable.PrimaryKey = new DataColumn[] { employeeTable.Columns["Id"] };

// 各列のプロパティ設定
employeeTable.Columns["Id"].AllowDBNull = false; // Null不可
employeeTable.Columns["Id"].Unique = true;       // 重複不可
employeeTable.Columns["Name"].MaxLength = 50;    // 最大文字数

データの追加・更新・削除(DataRowの操作)

構造が定義できたら、次は実際のデータ(行)を操作します。

DataTableのデータは DataRow オブジェクトとして扱われます。

データの追加

新しい行を追加するには、 NewRow() メソッドで新しい行インスタンスを作成し、値をセットした後に Rows.Add() でテーブルに追加します。

C#
// 新しい行を作成
DataRow row = employeeTable.NewRow();
row["Id"] = 101;
row["Name"] = "田中 太郎";
row["Department"] = "開発部";
row["JoinedDate"] = new DateTime(2023, 4, 1);

// テーブルに行を追加
employeeTable.Rows.Add(row);

// 配列形式で直接追加することも可能
employeeTable.Rows.Add(102, "佐藤 花子", "人事部", new DateTime(2024, 1, 15));

データの更新

既存の行を更新するには、インデックスや主キー検索で行を特定し、値を直接書き換えます。

C#
// 0番目の行を更新
employeeTable.Rows[0]["Department"] = "営業部";

// 主キーを使用して行を検索し更新
DataRow targetRow = employeeTable.Rows.Find(101);
if (targetRow != null)
{
    targetRow["Name"] = "田中 次郎";
}

データの削除

行を削除するには、 Delete() メソッドまたは Remove() メソッドを使用します。

  • Delete(): 行に「削除フラグ」を立てます。データベースへの反映が必要な場合に使用します。
  • Remove(): DataTableから完全に行を削除します。
C#
// 特定の行をマーク(削除予約)
employeeTable.Rows[0].Delete();

// 完全に削除(インデックス指定)
employeeTable.Rows.RemoveAt(1);

// 変更を確定(Deleteされた行が消去される)
employeeTable.AcceptChanges();

データの検索とフィルタリング

DataTable内の特定のデータにアクセスする方法はいくつかあります。

小規模なデータであればループ処理で十分ですが、大量のデータを扱う場合は、組み込みの検索機能を利用するのが効率的です。

Selectメソッドによるフィルタリング

Select メソッドを使用すると、SQLのWHERE句のような構文で条件に合致する行を取得できます。

C#
// 開発部に所属する社員を検索
DataRow[] results = employeeTable.Select("Department = '開発部'");

// 複数の条件とソート
DataRow[] filteredResults = employeeTable.Select("Id > 100 AND Name LIKE '田%'", "JoinedDate DESC");

foreach (DataRow dr in filteredResults)
{
    Console.WriteLine($"{dr["Id"]}: {dr["Name"]}");
}

DataViewを使用したフィルタリングとソート

UI(Windows FormsやWPF)に表示する場合など、フィルタリングされた状態を保持したいときは DataView が便利です。

C#
DataView view = new DataView(employeeTable);

// フィルタ条件の設定
view.RowFilter = "Id > 100";

// ソート条件の設定
view.Sort = "Name ASC";

// フィルタリング後の行数を確認
Console.WriteLine($"該当件数: {view.Count}");

LINQ to DataSetを活用した高度なクエリ

.NET Framework 3.5以降、 LINQ to DataSet の登場により、DataTableの操作は劇的に進化しました。

文字列ベースの Select メソッドとは異なり、コンパイル時のチェックが可能で、型安全なコードを書くことができます。

AsEnumerable()の利用

DataTableでLINQを使用するには、 AsEnumerable() メソッドを呼び出す必要があります。

これにより、各行が IEnumerable<DataRow> として扱えるようになります。

C#
using System.Linq;
using System.Data;

// LINQを使用してデータを抽出
var query = from emp in employeeTable.AsEnumerable()
            where emp.Field<int>("Id") > 100
            orderby emp.Field<DateTime>("JoinedDate") descending
            select new
            {
                Id = emp.Field<int>("Id"),
                Name = emp.Field<string>("Name")
            };

foreach (var item in query)
{
    Console.WriteLine($"ID: {item.Id}, 名前: {item.Name}");
}

Field<T> と SetField<T>

LINQ操作においては、 Field<T> メソッドを使用して値を取得し、 SetField<T> メソッドで値を更新するのが推奨されます。

これらは DBNull を適切に処理してくれるため、キャストエラーを防ぐことができます。

C#
// 安全な値の取得
int id = row.Field<int>("Id");
string name = row.Field<string>("Name");

// 安全な値の設定
row.SetField<string>("Department", "広報部");

LINQの結果をDataTableに戻す

LINQで抽出した結果を再びDataTableとして保持したい場合は、 CopyToDataTable() メソッドを使用します。

C#
// 抽出結果から新しいDataTableを作成
DataTable subTable = employeeTable.AsEnumerable()
                        .Where(r => r.Field<string>("Department") == "開発部")
                        .CopyToDataTable();

DataTableと外部データの連携

DataTableは、外部データとの橋渡しとしても頻繁に利用されます。

ここでは、実務でよく遭遇する変換処理について解説します。

List<T> への変換

現代のC#開発では、DataTableよりも List<T> のようなコレクションが好まれます。

DataTableからクラスのリストに変換する方法は以下の通りです。

C#
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// DataTableをList<Employee>に変換
var employeeList = employeeTable.AsEnumerable().Select(row => new Employee
{
    Id = row.Field<int>("Id"),
    Name = row.Field<string>("Name")
}).ToList();

DataTableからJSONへの変換

Web APIなどでデータをやり取りする際、DataTableをJSON形式に変換する必要があります。

一般的なライブラリである Newtonsoft.Json (Json.NET) を使用すると、非常に簡単に変換できます。

C#
using Newtonsoft.Json;

// DataTableをJSON文字列に変換
string json = JsonConvert.SerializeObject(employeeTable, Formatting.Indented);
Console.WriteLine(json);

CSVデータとの連携

CSVファイルを読み込んでDataTableに格納する処理は、バッチ処理などで定番の実装です。

C#
// CSV読み込みの簡易例
public DataTable ReadCsv(string filePath)
{
    DataTable dt = new DataTable();
    string[] lines = System.IO.File.ReadAllLines(filePath);

    if (lines.Length > 0)
    {
        // ヘッダー行から列を作成
        string[] headers = lines[0].Split(',');
        foreach (string header in headers)
            dt.Columns.Add(header.Trim());

        // データ行の追加
        for (int i = 1; i < lines.Length; i++)
        {
            dt.Rows.Add(lines[i].Split(','));
        }
    }
    return dt;
}

実践的なコード例:一連の操作

これまでに解説した内容を統合した、完全なサンプルコードを以下に示します。

C#
using System;
using System.Data;
using System.Linq;

namespace DataTableExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1. DataTableの初期化
            DataTable table = CreateSampleTable();

            // 2. データの追加
            AddSampleData(table);

            // 3. LINQを使用した検索
            Console.WriteLine("--- 30歳以上の社員一覧 ---");
            var filtered = table.AsEnumerable()
                                .Where(r => r.Field<int>("Age") >= 30)
                                .OrderBy(r => r.Field<int>("Age"));

            foreach (var row in filtered)
            {
                Console.WriteLine($"ID: {row.Field<int>("Id")}, 名前: {row.Field<string>("Name")}, 年齢: {row.Field<int>("Age")}");
            }

            // 4. データの更新
            DataRow target = table.Select("Id = 2").FirstOrDefault();
            if (target != null)
            {
                target.SetField("Name", "佐藤 修正済み");
            }

            // 5. 最終的な結果出力
            Console.WriteLine("\n--- 全データ ---");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine($"{row["Id"]}\t{row["Name"]}\t{row["Age"]}");
            }
        }

        static DataTable CreateSampleTable()
        {
            DataTable dt = new DataTable("Users");
            dt.Columns.Add("Id", typeof(int));
            dt.Columns.Add("Name", typeof(string));
            dt.Columns.Add("Age", typeof(int));
            dt.PrimaryKey = new DataColumn[] { dt.Columns["Id"] };
            return dt;
        }

        static void AddSampleData(DataTable dt)
        {
            dt.Rows.Add(1, "山田 太郎", 25);
            dt.Rows.Add(2, "佐藤 花子", 34);
            dt.Rows.Add(3, "鈴木 一郎", 45);
            dt.Rows.Add(4, "伊藤 京子", 29);
        }
    }
}
実行結果
--- 30歳以上の社員一覧 ---
ID: 2, 名前: 佐藤 花子, 年齢: 34
ID: 3, 名前: 鈴木 一郎, 年齢: 45

--- 全データ ---
1	山田 太郎	25
2	佐藤 修正済み	34
3	鈴木 一郎	45
4	伊藤 京子	29

パフォーマンスを最適化するためのテクニック

DataTableは便利な反面、大量のデータを扱う際にはメモリ消費量や処理速度が問題になることがあります。

以下のポイントを意識することで、パフォーマンスの低下を抑えることが可能です。

BeginLoadData と EndLoadData の活用

大量の行を一括で追加する場合、インデックスの更新やイベントの発生を一時的に停止させることで、処理を高速化できます。

C#
table.BeginLoadData();
try
{
    for (int i = 0; i < 10000; i++)
    {
        table.Rows.Add(i, "User" + i, 20);
    }
}
finally
{
    table.EndLoadData();
}

インデックスの活用

Select メソッドや Rows.Find メソッドを頻繁に使用する場合、主キー(PrimaryKey)を設定しておくことが重要です。

主キーが設定されていると、内部的にインデックスが作成されるため、検索スピードが飛躍的に向上します。

不要な制約チェックの無効化

データの流し込み(インポート)時など、一時的に整合性チェックが不要な場合は、 EnforceConstraints プロパティを false に設定することを検討してください。

C#
DataSet ds = new DataSet();
ds.Tables.Add(table);
ds.EnforceConstraints = false; // 制約チェックを一時停止
// 大量データ処理
ds.EnforceConstraints = true;  // 処理後に再開

DataTableとEntity Frameworkの比較

現代の開発では「どちらを使うべきか」という議論がよく行われます。

それぞれの特性を理解し、適切に使い分けましょう。

特徴DataTableEntity Framework (EF Core)
スキーマの定義実行時に動的に作成可能コンパイル時にクラスとして定義
柔軟性非常に高い(列を自由に追加可能)低い(モデルの変更が必要)
型安全性低い(文字列指定が多い)非常に高い
メモリ使用量比較的多い最適化されている
主な用途CSV加工、動的レポート、一時保持DBアプリケーション、ビジネスロジック

基本的には、データベースとの連携が中心のアプリケーションであれば Entity Framework を推奨します。

一方で、SQLの実行結果を型を決めずに汎用的に受け取りたい場合や、実行時にユーザーが定義した項目を扱うようなシステムでは、 DataTable が依然としてベストな選択肢となります。

まとめ

C#のDataTableは、データの構造化とメモリ上での管理を簡便に行える非常に汎用性の高いクラスです。

初期化からデータの追加・更新・削除といった基本操作、さらにはLINQを用いた高度なクエリや外部データとの連携まで、その活用範囲は多岐にわたります。

特に、AsEnumerable()によるLINQ活用 は、DataTableの弱点であった型安全性の低さを補い、現代的なクリーンなコード記述を可能にします。

また、大規模データを扱う際には、インデックスの設定や負荷軽減のためのメソッドを適切に組み合わせることが重要です。

Entity Frameworkなどの新しい技術が登場してもなお、DataTableが現場で使われ続けているのは、その「圧倒的な柔軟性」があるからです。

本記事で紹介したテクニックを駆使して、状況に応じた最適なデータ管理を実現してください。