【问题标题】:Join 2 DataTables on dynamic number of columns在动态列数上加入 2 个数据表
【发布时间】:2016-08-04 22:17:58
【问题描述】:

我正在尝试在动态列数上加入两个数据表。我已经了解了下面的代码。问题是连接的 ON 语句。如何根据列表“joinColumnNames”中的列名数量来实现这一动态。

我在想我需要构建某种表达式树,但我找不到任何示例说明如何使用多个连接列以及每个列都没有属性的 DataRow 对象来执行此操作。

private DataTable Join(List<string> joinColumnNames, DataTable pullX, DataTable pullY)
{
    DataTable joinedTable = new DataTable();

    // Add all the columns from pullX
    foreach (string colName in joinColumnNames)
    {
        joinedTable.Columns.Add(pullX.Columns[colName]);
    }
    // Add unique columns from PullY
    foreach (DataColumn col in pullY.Columns)
    {
        if (!joinedTable.Columns.Contains((col.ColumnName)))
        {
            joinedTable.Columns.Add(col);
        }
    }

    var Join = (from PX in pullX.AsEnumerable()
                join PY in pullY.AsEnumerable() on 
                // This must be dynamic and join on every column mentioned in joinColumnNames
                new { A = PX[joinColumnNames[0]], B = PX[joinColumnNames[1]] } equals new { A = PY[joinColumnNames[0]], B = PY[joinColumnNames[1]] } 
                into Outer
                from PY in Outer.DefaultIfEmpty<DataRow>(pullY.NewRow())
                select new { PX, PY });

    foreach (var item in Join)
    {
        DataRow newRow = joinedTable.NewRow();
        foreach (DataColumn col in joinedTable.Columns)
        {
            var pullXValue = item.PX.Table.Columns.Contains(col.ColumnName) ? item.PX[col.ColumnName] : string.Empty;
            var pullYValue = item.PY.Table.Columns.Contains(col.ColumnName) ? item.PY[col.ColumnName] : string.Empty;
            newRow[col.ColumnName] = (pullXValue == null || string.IsNullOrEmpty(pullXValue.ToString())) ? pullYValue : pullXValue;
        }
        joinedTable.Rows.Add(newRow);
    }

    return joinedTable;
}

添加一个特定示例以使用 3 个连接列(国家、公司和日期 ID)显示输入/输出:

拉 X:

国家公司 DateId 销售
美国 Test1 Ltd 20160722 25 美元

加拿大 Test3 Ltd 20160723 30 美元

意大利 Test4 Ltd 20160724 40 美元

印度 Test2 Ltd 20160725 35 美元

拉 Y:

国家公司 DateId 下载
美国Test1 Ltd 20160722 500

墨西哥 Test2 Ltd 20160723 300

意大利Test4 Ltd 20160724 900

结果:

国家 公司 DateId 销售 下载 美国 Test1 Ltd 20160722 25 500 美元 加拿大 Test3 Ltd 20160723 30 美元 墨西哥 Test2 Ltd 20160723 300 意大利 Test4 Ltd 20160724 40 900 美元 印度 Test2 Ltd 20160725 35 美元

【问题讨论】:

  • 你能给出一个示例输入和输出吗?不在“joinColumnNames”中的“pullX”列呢?您不想在输出中包含这些列吗?
  • 是的,输出应该包含来自 x 和 y 的所有列。我添加了一个具体的例子。

标签: c# linq datatable expression-trees


【解决方案1】:
var Join = 
    from PX in pullX.AsEnumerable()
    join PY in pullY.AsEnumerable()
    on     string.Join("\0", joinColumnNames.Select(c => PX[c]))
    equals string.Join("\0", joinColumnNames.Select(c => PY[c]))
    into Outer
    from PY in Outer.DefaultIfEmpty<DataRow>(pullY.NewRow())
    select new { PX, PY };

另一种方法是在 DataSet 中同时使用 DataTable 并使用 DataRelation
How To: Use DataRelation to perform a join on two DataTables in a DataSet?

【讨论】:

  • 是的!谢谢,效果很好。它比我想的要简单得多:)
  • 我很高兴它成功了,因为我从未使用过 join 也没有对其进行测试:]
【解决方案2】:

由于您使用的是 LINQ to Objects,因此无需使用表达式树。您可以使用自定义相等比较器解决您的问题。

创建一个相等比较器,它可以根据特定列的值比较两个DataRow 对象之间的相等性。这是一个例子:

public class MyEqualityComparer : IEqualityComparer<DataRow>
{
    private readonly string[] columnNames;

    public MyEqualityComparer(string[] columnNames)
    {
        this.columnNames = columnNames;
    }

    public bool Equals(DataRow x, DataRow y)
    {
        return columnNames.All(cn => x[cn].Equals(y[cn]));
    }

    public int GetHashCode(DataRow obj)
    {
        unchecked
        {
            int hash = 19;
            foreach (var value in columnNames.Select(cn => obj[cn]))
            {
                hash = hash * 31 + value.GetHashCode();
            }
            return hash;
        }
    }
}

然后你可以使用它来进行这样的连接:

public class TwoRows
{
    public DataRow Row1 { get; set; }
    public DataRow Row2 { get; set; }
}

private static List<TwoRows> LeftOuterJoin(
    List<string> joinColumnNames,
    DataTable leftTable,
    DataTable rightTable)
{
    return leftTable
        .AsEnumerable()
        .GroupJoin(
            rightTable.AsEnumerable(),
            l => l,
            r => r,
            (l, rlist) => new {LeftValue = l, RightValues = rlist},
            new MyEqualityComparer(joinColumnNames.ToArray()))
        .SelectMany(
            x => x.RightValues.DefaultIfEmpty(rightTable.NewRow()),
            (x, y) => new TwoRows {Row1 = x.LeftValue, Row2 = y})
        .ToList();
}

请注意,我使用的是方法语法,因为我认为您不能使用自定义相等比较器。

请注意,该方法执行左外连接,而不是完全外连接。根据您提供的示例,您似乎想要一个完整的外部联接。为此,您需要执行两个左外连接(请参阅此answer)。下面是完整方法的样子:

private static DataTable FullOuterJoin(
    List<string> joinColumnNames,
    DataTable pullX,
    DataTable pullY)
{
    var pullYOtherColumns =
        pullY.Columns
            .Cast<DataColumn>()
            .Where(x => !joinColumnNames.Contains(x.ColumnName))
            .ToList();

    var allColumns = 
        pullX.Columns
            .Cast<DataColumn>()
            .Concat(pullYOtherColumns)
            .ToArray();

    var allColumnsClone =
        allColumns
            .Select(x => new DataColumn(x.ColumnName, x.DataType))
            .ToArray();

    DataTable joinedTable = new DataTable();

    joinedTable.Columns.AddRange(allColumnsClone);

    var first =
        LeftOuterJoin(joinColumnNames, pullX, pullY);

    var resultRows = new List<DataRow>();

    foreach (var item in first)
    {
        DataRow newRow = joinedTable.NewRow();
        foreach (DataColumn col in joinedTable.Columns)
        {
            var value = pullX.Columns.Contains(col.ColumnName)
                ? item.Row1[col.ColumnName]
                : item.Row2[col.ColumnName];

            newRow[col.ColumnName] = value;
        }
        resultRows.Add(newRow);
    }

    var second =
        LeftOuterJoin(joinColumnNames, pullY, pullX);

    foreach (var item in second)
    {
        DataRow newRow = joinedTable.NewRow();
        foreach (DataColumn col in joinedTable.Columns)
        {
            var value = pullY.Columns.Contains(col.ColumnName)
                ? item.Row1[col.ColumnName]
                : item.Row2[col.ColumnName];

            newRow[col.ColumnName] = value;
        }
        resultRows.Add(newRow);
    }

    var uniqueRows =
        resultRows
            .Distinct(
                new MyEqualityComparer(
                    joinedTable.Columns
                        .Cast<DataColumn>()
                        .Select(x => x.ColumnName)
                        .ToArray()));

    foreach (var uniqueRow in uniqueRows)
        joinedTable.Rows.Add(uniqueRow);


    return joinedTable;
}

另请注意我如何克隆列。您不能在两个表中使用相同的列对象。

【讨论】:

  • 非常彻底和完整的解决方案,我喜欢它。感谢您指出列克隆问题和我的左连接而不是完全外连接。我最终做了一件非常相似的事情。我左连接 x 和 y,左连接 y 和 x,然后我使用联合。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-18
  • 1970-01-01
  • 2012-10-20
  • 1970-01-01
  • 1970-01-01
  • 2012-10-17
  • 1970-01-01
相关资源
最近更新 更多