簡體   English   中英

比較兩個 LINQ 查詢並在數據網格視圖中獲取差異 (C#)

[英]Compare two LINQ queries and get differences in a datagridview (C#)

我有兩個 C# 查詢結果(具有相同的屬性)。 我想比較它們並在 DataGridview 中顯示差異(添加/刪除的項目以及修改的屬性)。 是這樣的:

在此處輸入圖像描述

實現這一目標的最佳方法是什么?

以下可能是一種可能的方法(不聲稱這是最好的方法)。

假設你的class實現如下:

public class Item
{
    public int Id { get; set; }
    public int Weight { get; set; }
    public int Phase { get; set; }
}

您可以實施一個比較 class 來比較上一個項目(可能是null )和當前項目(可能是null ):

public class ItemComparison
{
    public int Id { get; init; }
    
    public string Weight => GetComparisonString(_weightPrevious, _weightCurrent);
    public string Phase => GetComparisonString(_phasePrevious, _phaseCurrent);
    
    public ItemComparison(Item previousItem, Item currentItem)
    {
        // TODO Error handling: previousItem and currentItem may
        //  - both be null
        //  - both be not null and have different Id values
        
        Id = (previousItem ?? currentItem).Id;
        
        _weightPrevious = previousItem?.Weight;
        _weightCurrent = currentItem?.Weight;
        _phasePrevious = previousItem?.Phase;
        _phaseCurrent = currentItem?.Phase;
    }
    
    private int? _weightPrevious;
    private int? _weightCurrent;
    private int? _phasePrevious;
    private int? _phaseCurrent;
    
    private string GetComparisonString(int? previousValue, int? currentValue)
    {
        if (previousValue == currentValue)
        {
            return "not changed";
        }
        
        return $"{GetStringOrUnknown(previousValue)} --> {GetStringOrUnknown(currentValue)}"; 
    }
    
    private string GetStringOrUnknown(int? value)
    {
        return value.HasValue ? value.ToString() : "?";
    }
}

並按如下方式計算更新的項目刪除的項目添加的項目

//using System.Collections.Generic;
//using System.Linq;

var updatedItems = queryResult1
    .Join(queryResult2,
        previous => previous.Id,
        current => current.Id,
        (previous, current) => new ItemComparison(previous, current));

var removedItems = queryResult1
    .ExceptBy(queryResult2.Select(qr2 => qr2.Id), qr1 => qr1.Id)
    .Select(removed => new ItemComparison(removed, null));

var addedItems = queryResult2
    .ExceptBy(queryResult1.Select(qr1 => qr1.Id), qr2 => qr2.Id)
    .Select(added => new ItemComparison(null, added));

最后,可以連接項目比較以產生最終比較結果:

List<ItemComparison> comparisonResult = updatedItems
    .Concat(removedItems)
    .Concat(addedItems)
    .OrderBy(item => item.Id)
    .ToList();

然后可以使用comparisonResult來填充您的DataGridView

示例小提琴在這里

假設 ID 永遠不會改變,我的建議是對 ID 進行完全外部連接

  • 添加的元素是查詢 1 中沒有但在查詢 2 中的元素
  • 刪除的元素是在查詢 1 中但不再在查詢 2 中的元素
  • 更改的元素是查詢 1 和 2 中都存在但值不相等的元素。

las, class Enumerable沒有用於完全外部連接的擴展方法。 幸運的是,創建一個相當容易。 如果您不熟悉擴展方法,請考慮閱讀Extension Methods Demystified (另一個使用方法語法而不是查詢語法的好理由)

全外連接

public static IEnumerable<TResult> FullOuterJoin<T1, T2, TKey, TResult>(
    this IEnumerable<T1> sequence1,
    IEnumerable<T2> sequence2,
    Func<T1, TKey> joinKey1Selector,
    Func<T2, TKey> joinKey2Selector,
    Func<T1, T2, TKey, TResult> resultSelector,
    IEqualityComparer<TKey> keyComparer)
{
    // TODO: implement
}

就像許多 LINQ 方法一樣,您可以通過編寫多個重載來幫助您的方法的用戶,例如,一個沒有keyComparer的重載:

public static IEnumerable<TResult> FullOuterJoin<T1, T2, TKey, TResult>(
    this IEnumerable<T1> sequence1,
    IEnumerable<T2> sequence2,
    Func<T1, TKey> joinKey1Selector,
    Func<T2, TKey> joinKey2Selector,
    Func<T1, T2, Tkey, TResult> resultSelector)
{
    return FullOuterJoin(sequence1, sequence2,
                         joinKey1Selector, joinKey2Selector,
                         resultSelector, null);
}

在您的問題中使用 FullOuterJoin

用法如下:

class QueryResult
{
    public int Id {get; set;}
    public decimal Weight {get; set;}           // maybe other type
    public int Phase {get; set;}
}

IEnumerable<QueryResult> query1 = ...
IEnumerable<QueryResult> query2 = ...

// full outer join query1 and query 2 on Id:
var fullOuterJoin = query1.FullOuterJoin(query2,

    queryResult => queryResult.Id,    // from every element from query1 take the Id
    queryResult => queryResult.Id,    // from every element from query2 take the Id

    // parameter resultSelector: from every T1 and its matching T2 make one new
    // note: T1 or T2 can be null (but not both)
    (x, y, key) => new
    {
        Id = key,
        Added = T1 == null,
        Removed = T2 == null,
        Changed = T1 != T2,

        Original = T1,
        Current = T2,
    };

檢測變化

我認為將屬性 Added / Removed / Changed 更改為枚舉會更整潔。 為此創建一個方法:

enum ChangeState {Unchanged, Added, Removed, Changed};

ChangeState DetectChange<T>(T x, T y)
{
    return DetectChange(x, y, null); // call the overload with comparer
}

ChangeState DetectChange<T>(T x, T y, IEqualityComparer<T> comparer)
{
    if (comparer == null) comparer = EqualityComparer<T>.Default;

    if (comparer.Equals(x, y)) return ChangeState.Unchanged;
    if (x == null) return ChangeState.Added;   // because y not null
    if (y == null) return ChangeState.Removed; // because x not null
    return ChangeState.Changed;
}

參數 resultSelector 將是這樣的:

(x, y, key) => new
{
    Id = key,
    ChangeState = DetectChange(x, y),
    ...

全外連接的實現

實現相當簡單:

  • 使用 TKey 作為鍵,為 sequence1 和 sequence2 創建字典。
  • 從兩個查找表中獲取所有不同的鍵
  • 對於每個鍵,從查找 X 和查找 Y 中獲取元素。這兩個中的一個可能是 null。
  • 使用resultSelector計算結果
  • yield 返回結果

.

public static IEnumerable<TResult> FullOuterJoin<Tx, Ty, TKey, TResult>(
    this IEnumerable<Tx> sequenceX,
    IEnumerable<Ty> sequenceY,
    Func<Tx, TKey> xKeySelector,
    Func<Ty, TKey> yKeySelector,
    Func<Tx, Ty, TKey, TResult> resultSelector,
    IEqualityComparer<TKey> keyComparer)
{
    // TODO: throw exception if any of sequenceX, sequenceY,
    // KeySelectors or resultSelector equal null

    // if keyComparer equals null, use default comparison technique
    if (keyComparer == null) keyComparer = EqualityComparer<TKey>.Default;

    // create two lookupTables:
    IDictionary<TKey, Tx> dictX = sequence1.ToDictionary(x => joinKey1Selector(x), keyComparer);
    IDictionary<TKey, Ty> dictY = sequence1.ToDictionary(y => joinKey2Selector(y), keyComparer);

    // get all used Tkey:
    IEnumerable<TKey> keysX= dictX.Select(x => x.Key);
    IEnumerable<TKey> keysY= dictY.Select(y => y.Key);
    IEnumerable<TKey> allUsedKeys = keysX.Union(keyY, keyComparer);

    // for every used key, get the x and the y and return a result
    foreach(TKey key in allUsedKeys)
    {
        dictX.TryGetValue(key, out Tx foundX);  // null if not found
        dictY.TryGetValue(key, out Ty foundY);
        TResult result = resultSelector(foundX, foundY, key);
        yield return result;
    }
}

IQueryable 問題

問題:完全外部連接的簡單擴展方法不適用於 IQueryable,僅適用於 IEnumerable。

如果您真的希望您的數據庫管理系統將完整的外部連接作為可查詢的,那么在將數據返回到您的本地進程之前,您必須創建一個以IQueryable<...>作為輸入的擴展方法。 也許這篇關於Left Outer Join as IQueryable的文章是一個很好的起點。

如果我查看建議的 Left Outer Join 的長度,我認為它不會非常有效。 考慮向 DbContext 添加一個執行完整外部聯接的方法,如 SQL 語句。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM