LINQで複数のプロパティをキーに重複を除去&子要素の先頭を取得


複数プロパティをキーに重複を除去

例として、以下のような「商品」クラスがあるとして

SalesItem.cs
    /// <summary>
    /// 商品
    /// </summary>
    public class SalesItem
    {
        /// <summary>
        /// ID
        /// </summary>
        [Key, Column(TypeName = "int")]
        public int Id { get; set; }

        /// <summary>
        /// 商品コード
        /// </summary>
        [Column(TypeName = "nvarchar")]
        public string SalesItemCode { get; set; }

        /// <summary>
        /// 商品名
        /// </summary>
        [Column(TypeName = "nvarchar")]
        public string SalesItemName { get; set; }
    }

商品コードと商品名で重複を除去して集計するには以下のようになる。

test.linq
// LINQPadでLanguageにC# Expressionを選んでいる場合の例。
SalesItems.GroupBy(s => new { s.SalesItemCode, s.SalesItemName }).Select(g => g.FirstOrDefault()).ToList().Dump() // LINQPad上で結果を見る場合は .Dump() を追加

なんだか若干分かりにくいが、上記のようにGroupBy、Select、FirstOrDefaultを組み合わせるとできる。
どうやらIGroupingインターフェイスをFirstOrDefaultすると最初のデータが取得されるらしい。
LINQ to ObjectsでもLINQ to EntitiesでもOK。
ちなみにEntity Frameworkの場合、LINQPadでSQLを確認すると以下のようなSQLが発行されている。

発行されるsql.sql
SELECT 
    [Limit1].[SalesItemId] AS [SalesItemId], 
    [Limit1].[SalesItemCode] AS [SalesItemCode], 
    [Limit1].[SalesItemName] AS [SalesItemName]
    FROM   (SELECT DISTINCT 
        [Extent1].[SalesItemCode] AS [SalesItemCode], 
        [Extent1].[SalesItemName] AS [SalesItemName]
        FROM [dbo].[SalesItems] AS [Extent1] ) AS [Distinct1]
    OUTER APPLY  (SELECT TOP (1) 
        [Extent2].[SalesItemId] AS [SalesItemId], 
        [Extent2].[SalesItemCode] AS [SalesItemCode], 
        [Extent2].[SalesItemName] AS [SalesItemName]
        FROM [dbo].[SalesItems] AS [Extent2]
        WHERE ([Distinct1].[SalesItemCode] = [Extent2].[SalesItemCode]) AND ([Distinct1].[SalesItemName] = [Extent2].[SalesItemName]) ) AS [Limit1]

※DISTINCTしたテーブルをOUTER APPLYしてSELECT TOP 1。
 うーん、あんまり見慣れない感じのSQLだけど集計効率的にはどうなのだろうか・・・

子要素の先頭を取得

あるデータに子要素が複数あって1件だけ取得したい場合も同様の実装で実現できる。
以下のようなUserクラスを内包するLoginクラスがあり、同じユーザーのレコードが複数あるとして、
※Userクラスの中身は割愛

Login.cs
    /// <summary>
    /// ログイン情報
    /// </summary>
    public class Login
    {
        /// <summary>
        /// ログインID
        /// </summary>
        [Key, Column(TypeName = "uniqueidentifier")]
        public Guid LoginId { get; set; }

        /// <summary>
        /// ログインユーザID
        /// </summary>
        [Column(TypeName = "int")]
        public int UserId { get; set; }

        [ForeignKey("UserId")]
        public User User { get; set; }
    }

Userごとの1件分のLoginを取得するコードは以下のようになる。

test.linq
// LINQPadでLanguageにC# Expressionを選んでいる場合の例。
Logins.GroupBy(l => l.UserId).Select(g => g.FirstOrDefault()).Dump() // LINQPad上で結果を見る場合は .Dump() を追加