DataTable の結合処理
複数の DataTable を統合する場景は頻繁に発生します。スキーマ(列構造)が一致している場合と異なっている場合で、アプローチが異なります。
1. スキーマが同一の場合
両方のテーブルが同じ列定義を持つ場合、データを単純に追加していくことで結合が可能です。
まず、検証用のデータを準備するメソッドを定義します。
private void SetupSampleTableA(DataTable table)
{
table.Columns.Add("employee_id");
table.Columns.Add("employee_name");
table.Rows.Add("E001", "Tanaka");
table.Rows.Add("E002", "Suzuki");
table.Rows.Add("E003", "Sato");
}
private void SetupSampleTableB(DataTable table)
{
table.Columns.Add("employee_id");
table.Columns.Add("employee_name");
table.Rows.Add("E101", "Yamada");
table.Rows.Add("E102", "Ito");
table.Rows.Add("E103", "Watanabe");
}
結合にはいくつかのアプローチがあります。一つ目は、構造のみをクローンし、行データを配列経由で追加する方法です。
var mergedTable = tableA.Clone();
var buffer = new object[mergedTable.Columns.Count];
// 表 A のデータ追加
foreach (DataRow row in tableA.Rows)
{
row.ItemArray.CopyTo(buffer, 0);
mergedTable.Rows.Add(buffer);
}
// 表 B のデータ追加
foreach (DataRow row in tableB.Rows)
{
row.ItemArray.CopyTo(buffer, 0);
mergedTable.Rows.Add(buffer);
}
二つ目は、Copy メソッドで構造とデータを複製し、ImportRow を使用して追加する方法です。こちらの方が簡潔です。
var mergedTable = tableA.Copy();
foreach (DataRow row in tableB.Rows)
{
mergedTable.ImportRow(row);
}
2. スキーマが異なる場合
列構造が異なる場合、横方向に列を拡張しながらデータを結合する必要があります。行数が一致しない場合の考慮も必要です。
private void SetupDifferentSchemaTableA(DataTable table)
{
table.Columns.Add("id_group_a");
table.Columns.Add("name_group_a");
table.Rows.Add("001", "Alice");
table.Rows.Add("002", "Bob");
}
private void SetupDifferentSchemaTableB(DataTable table)
{
table.Columns.Add("id_group_b");
table.Columns.Add("name_group_b");
table.Rows.Add("111", "Charlie");
table.Rows.Add("222", "David");
table.Rows.Add("333", "Eve");
}
以下のメソッドは、最初の表をベースにし、二つ目の表の列とデータを横に接続します。
private DataTable MergeHorizontal(DataTable sourceLeft, DataTable sourceRight, string tableName)
{
var resultTable = sourceLeft.Clone();
// 右側の表の列構造を追加
foreach (DataColumn col in sourceRight.Columns)
{
resultTable.Columns.Add(col.ColumnName, col.DataType);
}
// 左側のデータをコピー
foreach (DataRow row in sourceLeft.Rows)
{
resultTable.Rows.Add(row.ItemArray);
}
// 右側のデータを対応する行に埋め込む
int maxRows = Math.Max(sourceLeft.Rows.Count, sourceRight.Rows.Count);
// 行数が少ない場合に備えて行を追加
for (int i = sourceLeft.Rows.Count; i < maxRows; i++)
{
resultTable.Rows.Add(resultTable.NewRow());
}
// 右側データの値をセット
for (int i = 0; i < sourceRight.Rows.Count; i++)
{
for (int j = 0; j < sourceRight.Columns.Count; j++)
{
int targetColIndex = sourceLeft.Columns.Count + j;
resultTable.Rows[i][targetColIndex] = sourceRight.Rows[i][j];
}
}
resultTable.TableName = tableName;
return resultTable;
}
DataTable の比較処理
テーブル間の差分や共通データを抽出するには、LINQ を活用するのが効率的です。
完全一致による比較
構造が完全に一致している場合、Except や Intersect を直接使用できます。
// 除外リストに含まれないユーザーを抽出
var activeUsers = masterTable.AsEnumerable()
.Except(excludeTable.AsEnumerable(), DataRowComparer.Default);
// 両方に存在するユーザーを抽出
var commonUsers = powerUserTable.AsEnumerable()
.Intersect(activeUsers, DataRowComparer.Default);
var userList = new List<string>();
foreach (var row in commonUsers)
{
userList.Add(row.Field<string>("username"));
}
特定列による比較
一部の列のみをキーとして比較を行う場合は、Where 句と Contains を組み合わせます。
// 除外リストに用户名が含まれていないレコードを筛选
var validRecords = from row in masterTable.AsEnumerable()
where !excludeTable.AsEnumerable()
.Select(r => r.Field<string>("username"))
.Contains(row.Field<string>("username"))
select row;
// 一致するレコードからトークンを取得
var tokens = from row in validRecords
where powerUserTable.AsEnumerable()
.Select(r => r.Field<string>("username"))
.Contains(row.Field<string>("username"))
select row.Field<string>("token");
行と列の抽出操作
DataView を利用することで、元のテーブルを変更せずにフィルタリングや列の選択が可能です。
行のフィルタリング
RowFilter プロパティを使用して条件に合致する行のみを取得します。
DataView view = new DataView(sourceTable);
view.RowFilter = "Status = 'Active' AND Age > 20";
DataTable filteredTable = view.ToTable();
特定の列のみを抽出
ToTable メソッドの引数に列名を指定することで、必要な列のみを持つ新しいテーブルを作成できます。
DataTable subsetTable = sourceTable.DefaultView.ToTable(
false,
new string[] { "UserId", "UserName", "Email" }
);
列の順序変更と名前変更
列のインデックス順序を変更するには SetOrdinal を使用します。列名の変更は ColumnName プロパティで行います。
DataTable table = new DataTable();
table.Columns.Add("ColA");
table.Columns.Add("ColB");
table.Columns.Add("ColC");
// 3 列目を先頭に移動
table.Columns["ColC"].SetOrdinal(0);
// 列名の改名
table.Columns["ColA"].ColumnName = "Identifier";
新規列の追加と初期化
列を追加する際、DefaultValue を設定することで、既存の行に対する値の初期化を自動で行うことができます。
DataColumn countCol = new DataColumn();
countCol.ColumnName = "ProcessCount";
countCol.DataType = typeof(int);
countCol.DefaultValue = 0;
sourceTable.Columns.Add(countCol);