WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

各グループ内でレコードに連番を振る方法

by WebSurfer 28. February 2023 15:48

Linq の GroupBy を使ってグループ分けを行い、グループに属するレコード一覧を取得する際に 1, 2, 3 ... という連番を振る方法を書きます。

ASP.NET Core MVC アプリ

上の画像は ASP.NET Core MVC アプリのもので、Northwind サンプルデータベースの Products テーブルのレコードを Supplier でグループ分けし、各グループに含まれるレコードの ProductName と UnitPrice を取得するとともに ProductId 順に 1, 2, 3 ... という Index (連番) を振って表示したものです。

先の記事「Entity Framework で ROW_NUMBER」で、Linq to Objects で SQL 文の ROW_NUMBER を使った場合と同様な連番を振る方法を紹介しました。それを GroupBy でグループ分けした各グループ内で行うものです。

Linq の GroupBy メソッドを使ってグループ分けすると IEnumerable<IGrouping<TKey, TElement>> オブジェクトが得られます。IGrouping<TKey, TElement> オブジェクトは共通のキー TKey を持つ TElement のコレクションになります。

Linq の Select メソッドを使って IGrouping<TKey, TElement> オブジェクトを反復処理する際に TElement にアクセスして必要な値を取得できますが、それにオーバーロードの一つである Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,TResult>) を使うと、同時に 0 番から始まる連番の index を取得することができます。

上の画像の ASP.NET Core MVC アプリのコードを下に載せておきます。使用したコンテキストクラスとエンティティクラスは先の記事「Linq の GroupBy と Aggregate」のものと同じです。上に述べた連番を振る具体例は下のコードの最後の方の Controller / Action Method のコードを見てください。

Model

namespace MvcNet7App.Models
{
    public class ProductDTO
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; } = null!;
        public string Supplier { get; set; } = null!;
        public decimal? UnitPrice { get; set; }
        public int Index { get; set; } // 連番用
    }
}

View

@model Dictionary<string, IEnumerable<ProductDTO>>

@{
    ViewData["Title"] = "GroupBy";
}

@foreach (var item in Model)
{
    <h4>Supplier: @Html.DisplayFor(m => item.Value.First().Supplier)</h4>
    <table class="table">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(m => item.Value.First().ProductId)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.Value.First().Index)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.Value.First().ProductName)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.Value.First().UnitPrice)
                </th>
            </tr>
        </thead>
        <tbody>
            @foreach (var product in item.Value)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(m => product.ProductId)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.Index)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.ProductName)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.UnitPrice)
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

Controller / Action Method

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;
using System.ComponentModel.DataAnnotations;

namespace MvcNet7App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

        public ProductsController(NorthwindContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> GroupBy()
        {
            // いきなり _context.Products.GroupBy(p => p.SupplierId) と
            // はできないので一旦 List<Product> 型オブジェクトを作り、            
            List<Product> productList = await _context.Products
                                              .Include(p => p.Supplier)
                                              .ToListAsync();

            // Linq to Objects として GroupBy を適用する
            IEnumerable<IGrouping<string, Product>> groups = 
                productList.GroupBy(p => p.Supplier!.CompanyName)
                .OrderBy(g => g.Key);

            // グループごとに連番を取得して IEnumerable<ProductDTO> オブ
            // ジェクトを作り、Dictionary<string, IEnumerable<ProductDTO>>
            // に詰め替えて View にモデルとして渡す
            var dic = new Dictionary<string, IEnumerable<ProductDTO>>();
            foreach (IGrouping<string, Product> group in groups)
            {                
                var groupDTO = group
                       .OrderBy(p => p.ProductId)
                       .Select((p, index) => new ProductDTO
                       {
                           Supplier = p.Supplier!.CompanyName,
                           ProductId = p.ProductId,
                           Index = index + 1,
                           ProductName = p.ProductName,                           
                           UnitPrice = p.UnitPrice                           
                       });

                dic.Add(group.Key, groupDTO);
            }

            return View(dic);
        }
    }
}

Tags: , , ,

ADO.NET

Linq の GroupBy と Aggregate

by WebSurfer 24. February 2023 16:56

Linq の GroupBy を使ってグループ分けを行い、グループ分けに指定したフィールドや Sum メソッド等で取得できる集計値だけでなく、それ以外のフィールドの値を取得する方法を書きます。

ASP.NET Core MVC アプリ

SQL Server に投げる SQL 文でそのようなデータを取る方法は自分の知る限りなさそうですが、.NET アプリで Linq を使うと何とかなるということで、その方法を備忘録として書いておくことにしました。

上の画像は ASP.NET Core MVC アプリのもので、Northwind サンプルデータベースの Products テーブルのレコードを Supplier と Category でグループ化し、グループに含まれる製品価格の最小値と最大値をそれぞれ MinPrice と MaxPrice として表示すると共に、グループに含まれる製品の名前一覧をカンマ区切りで ProductNames に表示したものです。

ポイントは、グループ化された結果の IGrouping<TKey, TElement> オブジェクトから Select メソッドで ProductName のコレクションを取得し、それらを Enumerable.Aggregate メソッドを使って連結したところです。この記事の最後の方に載せた Controller / Action Method のコードを見てください。

上の画像の ASP.NET Core MVC アプリの���ードを下に載せておきます。

コンテキストクラスとエンティティクラスは、リバースエンジニアリングで既存の Northwind の Products, Categories, Supplers テーブルから生成したものです。

エンティティクラスの構造は以下のようになっています。赤茶色と赤はナビゲーションプロパティ、緑は主キーのプロパティ、青は FK 制約付きのフィールドのプロパティです。

エンティティクラス

Model

namespace MvcNet7App.Models
{    public class GroupedProduct
    {
        public string Supplier { get; set; } = null!;
        public string Category { get; set; } = null!;
        public decimal MaxPrice { get; set; }
        public decimal MinPrice { get; set; }
        public string ProductNames { get; set; } = null!;
    }
}

View

@model IEnumerable<GroupedProduct>

@{
    ViewData["Title"] = "GroupBy2";
}

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Supplier)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Category)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.MinPrice)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.MaxPrice)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ProductNames)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var product in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(m => product.Supplier)
                </td>
                <td>
                    @Html.DisplayFor(m => product.Category)
                </td>
                <td>
                    @Html.DisplayFor(m => product.MinPrice)
                </td>
                <td>
                    @Html.DisplayFor(m => product.MaxPrice)
                </td>
                <td>
                    @Html.DisplayFor(m => product.ProductNames)
                </td>
            </tr>
        }
    </tbody>
</table>

Controller / Action Method

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;

namespace MvcNet7App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

        public ProductsController(NorthwindContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> GroupBy2()
        {
            // 下のコードで使った Aggregate メソッドは SQL 文に
            // 変換できず Linq to Entities では使えないので、こ
            // こで List<Product> オブジェクトを取得し、
            var products = await _context.Products
                           .Include(p => p.Category)
                           .Include(p => p.Supplier)
                           .ToListAsync();

            // 以下で Linq to Objects として処理する
            var grouped = products
                .GroupBy(p => new
                {
                    p.Supplier!.CompanyName,
                    p.Category!.CategoryName
                })
                .Select(g => new GroupedProduct
                {
                    Supplier = g.Key.CompanyName,
                    Category = g.Key.CategoryName,
                    MaxPrice = g.Max(p => p.UnitPrice)!.Value,
                    MinPrice = g.Min(p => p.UnitPrice)!.Value,
                    ProductNames = g.Select(p => p.ProductName)
                                    .Distinct()
                                    .Aggregate((a, b) => $"{a}, {b}")
                })
                .OrderBy(gp => gp.Supplier);

            return View(grouped);
        }
    }
}

Tags: , ,

ADO.NET

SelectMany メソッド

by WebSurfer 21. March 2022 19:15

Linq の SelectMany メソッドについて調べて、多少なりとも分かったような気になったので、自分なりの理解を備忘録として書いておくことにしました。

顧客が過去に注文した製品一覧

自分の手を動かしてコードを書くと理解が深まるだろうと思って、Northwind サンプルデータベースの Customers テーブルの顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得するサンプルを作ってみました。上の画像がその結果を表示したものです。どのようなコードを書いたかは後述します。

Microsoft のドキュメント「Enumerable.SelectMany メソッド」を見ると "シーケンスの各要素を IEnumerable<T> に射影し、結果のシーケンスを 1 つのシーケンスに平坦化します。 Projects each element of a sequence to an IEnumerable<T> and flattens the resulting sequences into one sequence." と書いてあるのですが、自分の頭ではその説明ではさっぱり意味が分かりませんでした。「シーケンス (sequence)」って何? 「射影 (project)」って何? 「平坦化 (flatten)」ってどういうこと?・・・って感じ。(汗)

ググって調べてみると「シーケンス」というのは .NET の Linq の世界に限れば "IEnumerable または IEnumerable<T> インターフェイスを継承するオブジェクト" と理解すれば良さそうです。

「射影」というのは Microsoft のドキュメント「射影操作 (C#)」によれば "オブジェクトを必要なプロパティだけで構成された別の形式に変換する操作" ということだそうです。その際「平坦化」を同時に行うのが Select メソッドとは違う所のようです。

で、問題の「平坦化」ですが、これは BuildInsider の記事「LINQ:取得列を明示的に指定する - select句/SelectManyメソッド[C#]」の説明が分かりやすかったです。

下の画像は Northwind サンプルデータベースの Customers, Orders, Order_Details, Products テーブルから生成した Entity Data Model ですが、これを例に取って説明します。

Northwind EDM

Orders の中には複数の顧客の注文データが複数(過去の注文の数)含まれており、各注文に紐づく詳細は Order_Details に含まれています。Order_Details のデータは Orders のナビゲーションプロパティ Order_Details から取得できます。

Orders から CustomerID が "ALFKI" の顧客の注文(Orders の中に複数あります)を抽出し、それに紐づく Order_Details を Select および SelectMany メソッドで引数にナビゲーションプロパティ Order_Details 設定して取得してみます。

Select メソッド

結果のオブジェクトが List<ICollection<OrderDetail>> 型となっています。上に紹介した BuildInsider の記事にも書いてありますように、OrderDetail にアクセスするためには 2 回ループを回す必要があります。

Select の結果

SelectMany メソッド

結果のオブジェクトが List<OrderDetail> 型になっており「平坦化」されているのが分かるでしょうか?

SelectMany の結果

ちなみに SelectMany メソッドの引数に指定するナビゲーションプロパティは IEnumerable<T> 型でなければならないので注意してください。間違って他の Employee 型とかのプロパティを設定すると以下のようなエラーが出ます。(意味不明なので悩むかも。何を隠そう自分がそうでした)

"エラー CS0411 メソッド 'Enumerable.SelectMany<TSource, TResult>(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)' の型引数を使い方から推論することはできません。型引数を明示的に指定してください。"


もう一つ、上の例より実用的かもしれないサンプルコードを載せておきます。 ASP.NET Core MVC アプリで、Customers テーブル顧客一覧を表示し (Customers/Index)、一覧の中から選んだ特定の顧客が過去に注文した製品の一覧を SelectMany メソッドと GroupBy メソッドを使って取得し、ViewData を使って View に渡して表示するもので (Customers/Details)、この記事の一番上の画像がその結果です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcCore6App2.Data;

namespace MvcCore6App2.Controllers
{
    public class CustomersController : Controller
    {
        private readonly NorthwindContext _context;

        public CustomersController(NorthwindContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> Index()
        {
            return View(await _context.Customers.ToListAsync());
        }

        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var customer = await _context.Customers
                .Include(c => c.Orders)
                    .ThenInclude(o => o.OrderDetails)
                        .ThenInclude(od => od.Product)
                .FirstOrDefaultAsync(c => c.CustomerId == id);

            var orderDetails = customer?.Orders
                .SelectMany(o => o.OrderDetails);

            if (orderDetails != null)
            {
                ViewData["PastOrderedProducts"] = orderDetails
                    .GroupBy(od => od.Product)
                    .Select(g => new PastOrderedProducts
                    { 
                        ProductId = g.Key.ProductId,
                        ProductName = g.Key.ProductName,
                        Quantity = g.Sum(g => g.Quantity) 
                    }).ToList();
            }

            if (customer == null)
            {
                return NotFound();
            }

            return View(customer);
        }
    }

    public class PastOrderedProducts
    {
        public int ProductId { get; set; }
        public string? ProductName { get; set; }
        public int Quantity { get; set; }
    }
}

Tags: , ,

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2023  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar