Sto cercando di unire due DataTable su un numero dinamico di colonne. Sono arrivato fino al codice qui sotto. Il problema è l'istruzione ON del join. Come posso rendere questa dinamica in base al numero di nomi di colonne presenti nell'elenco "joinColumnNames".
Stavo pensando che avrò bisogno di costruire una sorta di albero delle espressioni, ma non riesco a trovare alcun esempio su come farlo con più colonne di join e con l'oggetto DataRow che non ha proprietà per ogni colonna.
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;
}
Aggiunta di un esempio specifico per mostrare input / output utilizzando 3 colonne di join (Paese, Società e DateId):
Pull X:
Country Company DateId Sales United States Test1 Ltd 20160722 $25 Canada Test3 Ltd 20160723 $30 Italy Test4 Ltd 20160724 $40 India Test2 Ltd 20160725 $35
Pull Y:
Country Company DateId Downloads United States Test1 Ltd 20160722 500 Mexico Test2 Ltd 20160723 300 Italy Test4 Ltd 20160724 900
Risultato:
Country Company DateId Sales Downloads United States Test1 Ltd 20160722 $25 500 Canada Test3 Ltd 20160723 $30 Mexico Test2 Ltd 20160723 300 Italy Test4 Ltd 20160724 $40 900 India Test2 Ltd 20160725 $35
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 };
Un altro modo è avere DataTable
in un DataSet
e usare DataRelation
Come: utilizzare DataRelation per eseguire un join su due DataTable in un DataSet?
Poiché stai utilizzando LINQ to Objects, non è necessario utilizzare gli alberi di espressione. Puoi risolvere il tuo problema con un comparatore di uguaglianza personalizzato.
Creare un comparatore di uguaglianza che possa confrontare l'uguaglianza tra due oggetti DataRow
base ai valori di colonne specifiche. Ecco un esempio:
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;
}
}
}
Quindi puoi usarlo per fare il join in questo modo:
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();
}
Si noti che sto usando la sintassi del metodo perché non penso che sia possibile usare diversamente un comparatore di uguaglianza personalizzato.
Si noti che il metodo esegue un join esterno sinistro, non un join esterno completo. In base all'esempio che hai fornito, sembra che tu voglia un join esterno completo. Per fare questo devi fare due join esterni a sinistra (vedi questa risposta ). Ecco come sarebbe il metodo completo:
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;
}
Si noti anche come clonare le colonne. Non è possibile utilizzare lo stesso oggetto colonna in due tabelle.