Linq to Objects で GroupBy メソッドを使って IEnumerable<IGrouping<TKey, TElement>> オブジェクトを取得できるということを備忘録として書いておきます。
SQL 文の GROUP BY 句はグループごとの集計を取るために使われるものと理解しています。例えば上の画像にあるように、Products テーブルから SupplierID 別に商品単価の最高と最低の値を取得するというように。(DB は Microsoft が提供するサンプルデータベース Northwind です)
なので、GROUP BY 句で指定したフィールド(上の例では SupplierID)以外や、最大値と最小値を返す MAX(), MIN()、合計を返す SUM() や平均を返す AVG() など以外を SELECT することはできません。
例えば、上の画像の SELECT クエリで ProductName を SELECT しようとすると "列 'NORTHWIND.dbo.Products.ProductName' は選択リスト内では無効です。この列は集計関数または GROUP BY 句に含まれていません" というエラーになります。
Linq でも同様で、下のような Select メソッドなしで使うものではないと思っていました。
var result = await _context.Products.GroupBy(p => p.SupplierId).
Select(g => new
{
Id = g.Key,
Max = g.Max(g => g.UnitPrice),
Min = g.Min(g => g.UnitPrice)
}).
ToListAsync();
Select メソッドなしで使うと、例えば以下のようにすると "Client side GroupBy is not supported" というエラーになります。(注: これは Core 3.0 以降の話で、Core 2.x 以前はサポートされていたそうです。.NET Framework ��� EF でもサポートされています。詳しくは Breaking changes included in EF Core 3.x の最初のセクション LINQ queries are no longer evaluated on the client を見てください)
var result1 = await _context.Products.GroupBy(p => p.SupplierId).
ToListAsync();
// または
var result2 = await (from p in _context.Products
group p by p.SupplierId into g
select g).ToListAsync();
そのエラーの原因は Linq to Entities で上のような GroupBy を使った Linq 式は SQL 文に変換できないということのようです。(ただし、Core 2.x 以前と .NET Framework では EF 側でオブジェクトの生成をサポートしていて、上記のコードでも OK です)
でも、Linq to Objects の場合は話が違ってきます。以下のコードは ASP.NET Core MVC のアクションメソッドですが、IEnumerable<IGrouping<int?, Products>> オブジェクトを model に取得して View に渡すことができています。
public async Task<IActionResult> ListBySupplier2()
{
// Linq to Objects で GroupBy を使うのは問題ない
List<Products> productList = await _context.Products.ToListAsync();
var model = productList.GroupBy(p => p.SupplierId);
return View(model);
}
Visual Studio 2019 のデバッガで上のコードの model を展開すると以下の画像のようになっています。
これが全くの予想外で驚いたので備忘録として残しておくことにした次第です。(自分が無知なだけということかもしれませんけど)
これが何の役に立つのかと言われると答えに窮しますが、例えば以下の View を使って SupplierID 別にテーブルを作ると言ったことができます。
@model IEnumerable<IGrouping<int?, Products>>
@{
ViewData["Title"] = "ListBySupplier2";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>ListBySupplier2</h1>
@foreach (var item in Model)
{
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(m => item.First().ProductId)
</th>
<th>
@Html.DisplayNameFor(m => item.First().ProductName)
</th>
<th>
@Html.DisplayNameFor(m => item.First().SupplierId)
</th>
<th>
@Html.DisplayNameFor(m => item.First().UnitPrice)
</th>
</tr>
</thead>
<tbody>
@foreach (var product in item)
{
<tr>
<td>
@Html.DisplayFor(m => product.ProductId)
</td>
<td>
@Html.DisplayFor(m => product.ProductName)
</td>
<td>
@Html.DisplayFor(m => product.SupplierId)
</td>
<td>
@Html.DisplayFor(m => product.UnitPrice)
</td>
</tr>
}
</tbody>
</table>
}
上のコードで foreach (var item in model) の item は IGrouping<int?, Products> に、foreach (var product in item) の product は Products になります。
結果、ブラウザには以下のように SupplierID 別のテーブルが表示されます。
ORDER BY [SupplierID] で並べて一つのテーブルに表示した方がスマートかつ見やすいと思いますが、それはとりあえず置いときます。(笑)