QA@IT

c#.net でDataTableの内容を比較する方法について

29256 PV

以下のDataTable1DataTable2を比較して不一致分を表示したいのですが
グルグルとループさせて1つずつ比較するぐらいしか方法を思いつきませんでした。
何かもっとスマートな方法はないでしょうか?

・DataTable1 … Oracleからの抽出結果を格納
・DataTable2 … system iからの抽出結果を格納
・不一致分をdatagridに表示
・.net FrameWork 2.0以上

よろしくお願いいたします。

回答

C#6.0でLinqを使って書いてみました。

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace LinqExcept {
    class Program {
        static void Main(string[] args) {
            using (DataTable dt1 = new DataTable())
            using (DataTable dt2 = new DataTable()) {
                dt1.Columns.Add("ID", typeof(int));
                dt1.Columns.Add("Name", typeof(string));

                dt1.Rows.Add(1, "佐藤");
                dt1.Rows.Add(2, "鈴木");
                dt1.Rows.Add(3, "渡辺");
                dt1.Rows.Add(4, "田中");

                dt2.Columns.Add("ID", typeof(int));
                dt2.Columns.Add("Name", typeof(string));

                dt2.Rows.Add(1, "鈴木");
                dt2.Rows.Add(2, "佐藤");
                dt2.Rows.Add(3, "渡部");
                dt2.Rows.Add(4, "田中");

                var query = dt1.AsEnumerable().Except(dt2.AsEnumerable(), new DataRowComparer());

                // ID=1 Name=佐藤
                // ID=2 Name=鈴木
                // ID=3 Name=渡辺
                query.ToList().ForEach(
                    r => Console.WriteLine(string.Format("ID={0} Name={1}", r["ID"], r["Name"])));
            }

            Console.ReadKey();
        }
    }

    class DataRowComparer : IEqualityComparer<DataRow> {
        public bool Equals(DataRow x, DataRow y) {
            if (object.ReferenceEquals(x, y))
                return true;

            if (x == null)
                return false;

            if (y == null)
                return false;

            bool equal = ((int) x["ID"] == (int) y["ID"]) && (x["Name"] == y["Name"]);
            return equal;
        }

        public int GetHashCode(DataRow obj) {
            if (obj == null)
                return 0;

            // C#5.0以前
            // int idHashCode = obj["ID"] == null ? 0 : obj["ID"].GetHashCode();
            // int nameHashCode = obj["Name"] == null ? 0 : obj["Name"].GetHashCode();
            int idHashCode = obj["ID"]?.GetHashCode() ?? 0;
            int nameHashCode = obj["Name"]?.GetHashCode() ?? 0;
            return idHashCode ^ nameHashCode;        
        }
    }
}
編集 履歴 (2)

JHashimoto さんの回答 を基に、C# 6 の機能を2つ追加して書いてみました。
(ワルノリしていろいろ使ってます)
細かく検証はしてませんが、結果は同じになるハズ。
EqualsはDBNull.Valueも想定するように直しています。

syntax highlight を C# 扱いにするとエラーになる(多分 String interpolation のあたり?)ので、
cにしてます。

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;

using static System.Console; // C# 6 using static

namespace LinqExceptEdited{
    class Program {
        static void Main(string[] args) {

            using (DataTable dt1 = new DataTable())
            using (DataTable dt2 = new DataTable()) {

                Action<DataTable, string[]> addItems = (dt, array) => {
                    array.Select((v, i) => new { v, i })
                        .ToList().ForEach(x => {
                            dt.Rows.Add(x.i + 1, x.v);
                        });
                };

                dt1.Columns.Add("ID", typeof(int));
                dt1.Columns.Add("Name", typeof(string));
                addItems(dt1, new[] { "佐藤", "鈴木", "渡辺", "田中" });

                dt2.Columns.Add("ID", typeof(int));
                dt2.Columns.Add("Name", typeof(string));
                addItems(dt2, new[] { "鈴木", "佐藤", "渡部", "田中" });

                var query = dt1.AsEnumerable().Except(dt2.AsEnumerable(), new DataRowComparer());

                // ID=1 Name=佐藤
                // ID=2 Name=鈴木
                // ID=3 Name=渡辺
                query.Select(r => $"ID={r["ID"]} Name={r["Name"]}").ToList().ForEach(WriteLine); // C# 6  String interpolation

            }
            ReadKey();
        }
    }

    class DataRowComparer : IEqualityComparer<DataRow> {
        public bool Equals(DataRow x, DataRow y) {
            return object.ReferenceEquals(x, y) ||
                (x != null && y != null && CompareAllColumns(x, y));
        }

        private bool CompareAllColumns(DataRow x, DataRow y) {
            var intColumns = new[] { "ID" };

            return new[] { "ID", "Name" }
                .Select(name => new { Name = name, X = x[name], Y = y[name], IsInt = intColumns.Contains(name) })
                .Aggregate(true, (acc, succ) => {
                    if (DBNull.Value.Equals(succ.X) || DBNull.Value.Equals(succ.Y)) return acc && (succ.X?.Equals(succ.Y) ?? false);
                    if (succ.IsInt) return acc && ((int)succ.X == (int)succ.Y);
                    return acc && (succ.X == succ.Y);
                });
        }

        public int GetHashCode(DataRow obj) {
            if (obj == null)
                return 0;

            // C#5.0以前
            // int idHashCode = obj["ID"] == null ? 0 : obj["ID"].GetHashCode();
            // int nameHashCode = obj["Name"] == null ? 0 : obj["Name"].GetHashCode();
            int idHashCode = obj["ID"]?.GetHashCode() ?? 0;
            int nameHashCode = obj["Name"]?.GetHashCode() ?? 0;
            return idHashCode ^ nameHashCode;
        }
    }
}

編集 履歴 (1)

環境に合うかわかりませんので、こんな方法もあります程度で。
結果を拾うために結局ぐるぐる回してますし。

実際のデータ構成やどう比較したいのかわからなかったので、全列をぶつける形にしています。
DataRelationを使って、外部キーのようなものを作成し、紐づかない行があれば差分アリという判断をしています。

Formにテキストボックスとボタンを置いて実行してみてください。
(VS2015で作成。)

C#2.0の場合は
, new[] { dt1.Columns["id"], dt1.Columns["value"] }
, new DataColumn[] { dt1.Columns["id"], dt1.Columns["value"] }にしないとビルドが通らないと思います。

DBの違いによってなにか起きる場合は DataTable#WriteXml, DataTable#ReadXmlでスキーマなしで保存・読み込みすれば緩和されるかもしれません。

以下のサンプルでは

  • idが奇数の時はvalueが一致しない
  • idが0のものは dt1にしか存在しない
  • idが7のものは dt2にしか存在しない

という状態のデータで比較しています。

主キーだけまたは特定の列だけの比較でよければ、DataRelation作成時に比較する列だけ指定してください。

namespace qait9488 {
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>データテーブルの作成</summary>
        private DataTable CreateTable(string tableName)
        {
            var dt = new DataTable(tableName);
            dt.Columns.Add("id", typeof(int));
            dt.Columns.Add("value", typeof(string));

            return dt;
        }

        /// <summary>データテーブルに値を追加</summary>
        private void AppendValues(DataTable dt, int fromId, int toId, bool doubleValueIfOdd)
        {
            for (var i = fromId; i < toId; i++)
            {
                var dr = dt.NewRow();
                dr["id"] = i;
                // doubleValueIfOdd が trueなら奇数の時 idの 2倍をvalueにする。
                dr["value"] = (doubleValueIfOdd && (i % 2 == 1)) ? (i * 2).ToString() : i.ToString();
                dt.Rows.Add(dr);
            }
            dt.AcceptChanges();
        }


        private void button1_Click(object sender, EventArgs e)
        {

            textBox1.Multiline = true;

            // サンプルテーブルの作成
            var dt1 = CreateTable("dt1");
            AppendValues(dt1, 0, 6, false); // ID 0 - 6,  valueはIDと同じ
            var dt2 = CreateTable("dt2");
            AppendValues(dt2, 1, 7, true);  // ID 1 - 7 , 奇数はIDの 2倍のvalue

            var ds = new DataSet();
            ds.Tables.Add(dt1);
            ds.Tables.Add(dt2);

            // 全ての列同士を結合するリレーションを作成
            var rel = new DataRelation("CompareRelation"
                , new[] { dt1.Columns["id"], dt1.Columns["value"] }
                , new[] { dt2.Columns["id"], dt2.Columns["value"] }
                , false);

            ds.Relations.Add(rel);

            var sb = new StringBuilder();

            foreach(DataRow dr in dt1.Rows) {
                // dt1の行に紐づく dt2の行数が 0件なら差分ありとする。
                if(dr.GetChildRows("CompareRelation").Length == 0) {
                    sb.AppendFormat("dt1 id:'{0}' は dt2と内容に差があるか、行が存在しません\r\n", dr["id"]);
                }
            }

            foreach (DataRow dr in dt2.Rows) {
                if (dr.GetParentRows("CompareRelation").Length == 0) {
                    sb.AppendFormat("dt2 id:'{0}' は dt1と内容に差があるか、行が存在しません\r\n", dr["id"]);
                }
            }

            textBox1.Text = sb.ToString();
        }

    }
}
編集 履歴 (0)

グルグルとループさせて1つずつ比較するぐらいしか方法を思いつきませんでした。
何かもっとスマートな方法はないでしょうか?

自分が知る限りですが(知らないだけという可能性は否定し切れませんが)ループを回して一つずつ調べていくほか方法はなさそうです。

検索すれば 2 つのテーブルから不一致分を取得する例が見つかると思います。例えば下記:

How to Compare two DataTables in ADO.net
http://canlu.blogspot.jp/2009/05/how-to-compare-two-datatables-in-adonet.html

datatable comparison などをキーワードに検索すると、他にも多々ヒットすると思いますのでお試しください。

編集 履歴 (1)

LINQを使って、集合演算をDataTableに適用するようなやり方が使えるかもしれません。
未確認ですが。
UnionしてDistinctしてIntercectをExceptする感じかな(適当)。
うまく行かなかったらすみません。

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。