WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

SelectMany メソッド (その 2)

by WebSurfer 2023年7月27日 10:11

先の記事「SelectMany メソッド」の続きです。

Microsoft のドキュメント「Enumerable.SelectMany メソッド」によるとこのメソッドには 4 つのオーバーロードがあります。それらの使い方を調べましたので、備忘録として書いておきます。

SQL Server サンプルデータベース Northwind の Orders, Order_Details テーブルから、リバースエンジニアリングで生成したコンテキスクラストとエンティティクラスをベースに使います。下の画像は Visual Studio 2022 の拡張機能 EF Core Power Tools を使って DbContext Diagram を表示したものです。

DbContext Diagram

Order には複数の顧客の過去の注文データ全てが含まれており、各注文に紐づく詳細は OrderDetails ナビゲーションプロパティをたどって OrderDetail にアクセスして取得できます。

Order から CustomerID が "ALFKI" の顧客の注文(Order の中に複数あります)を抽出し、それに紐づく OrderDetail を SelectMany メソッドで取得してみます。

以下に 4 つのオーバーロードを使った例を書きます。いずれも IEnumerable<T> を拡張する拡張メソッドであることに注意してください。

(1) その 1

// SelectMany<TSource,TResult>(
//     IEnumerable<TSource>,
//     Func<TSource,IEnumerable<TResult>>)
// シーケンスの各要素を IEnumerable<T> に射影し、結果のシー
// ケンスを 1 つのシーケンスに平坦化します。
var selectMany1 = await _context.Orders
    .Where(o => o.CustomerId == "ALFKI")
    .SelectMany(o => o.OrderDetails)
    .ToListAsync();

先の記事「SelectMany メソッド」に書いたのがこのメソッドです。

コードの上に書いたコメントは Microsoft のドキュメントの説明です。コメントの下のコードで具体的にどういうことをしているかと言うと:

Where メソッドの結果は IQueryable<Order> オブジェクト (IEnumerable<T> を継承) になります。これが上のコメント「シーケンスの各要素を・・・」の最初に出てくるシーケンスに該当します。各要素は Order オブジェクトです。

SelectMany メソッドは、その第 2 引数に指定された selector 関数 o => o.OrderDetails に従って、Order オブジェクトを必要なプロパティだけで構成された別の形式に変換し (これを「投射」という。この例では OrderDetails ナビゲーションプロパティで ICollection<OrderDetail> を取得)、それを 1 つのシーケンスに平坦化して返します (この例では IQueryable<OrderDetail> を返します)。

・・・ということで、上のコードの実行結果は以下の通りとなります。

その 1 の結果

(2) その 2

// SelectMany<TSource,TCollection,TResult>(
//     IEnumerable<TSource>,
//     Func<TSource,IEnumerable<TCollection>>,
//     Func<TSource,TCollection,TResult>)
// シーケンスの各要素を IEnumerable<T> に射影し、結果のシー
// ケンスを 1 つのシーケンスに平坦化して、その各要素に対し
// て結果のセレクター関数を呼び出します。
var selectMany2= await _context.Orders
    .Where(o => o.CustomerId == "ALFKI")
    .SelectMany(o => o.OrderDetails,
      (o, od) => new { o.OrderId, od.ProductId, od.UnitPrice })
    .ToListAsync();

上の「その 1」との違いは、SelectMany メソッドの引数に collectionSelector, resultSelector という 2 つの関数を取ることです。collectionSelector 関数を使って平坦化された中間シーケンスを生成し、次に resultSelector 関数を使って中間シーケンスの中の Order オブジェクトと OrderDetail オブジェクトの両方にアクセスして値を取得し、さらに別のシーケンス (上の例では IQueryable<匿名型>) を生成して戻り値として返しています。

上のコードの実行結果は以下の通りとなります。

その 2 の結果

上のコード例では Order オブジェクトの OrderId を取得しているところに注目してください。「その 1」のオーバーロードではそれはできません。

(3) その 3

// SelectMany<TSource,TResult>(
//     IEnumerable<TSource>,
//     Func<TSource,Int32,IEnumerable<TResult>>)
// 上の「その 1」と同様。加えてオーダー毎の index を付与。
// Linq to Entities では使えないので注意。
// index はオーダー毎に振られることに注意。
// 一つのオーダーは複数の OrderDetails を持つので下の例
// では index が同じになるものがある
var selectMany3 = _context.Orders.ToList()
    .Where(o => o.CustomerId == "ALFKI")
    .SelectMany((o, index) => o.OrderDetails
      .Select(od => new { index, od.ProductId, od.UnitPrice }))
    .ToList();

上の「その 1」とほぼ同様な操作を行いますが、加えてオーダー毎の index を付与できるところが異なります。

このオーバーロードは SQL Server などの DB で使われる SQL に変換できないので、Linq to Entities では使えないことに注意してください。

上のコードの実行結果は以下の通りとなります。

その 3 の結果

SelectMany で index を振ってどういう使い道があるかは自分的には謎です。先の記事「Entity Framework で ROW_NUMBER」で書いたようなケースでは意味があると思いますが。

(4) その 4

// SelectMany<TSource,TCollection,TResult>(
//     Enumerable<TSource>,
//     Func<TSource,Int32,IEnumerable<TCollection>>,
//     Func<TSource,TCollection,TResult>)
// 上の「その 2」と同様。加えてオーダー毎の index を付与。
// Linq to Entities では使えないので注意。
// index はオーダー毎に振られることに注意。
// 一つのオーダーは複数の OrderDetails を持つので下の例
// では index が同じになるものがある
var selectMany4 = _context.Orders.ToList()
    .Where(o => o.CustomerId == "ALFKI")
    .SelectMany((o, index) => o.OrderDetails
      .Select(od => new { index, od.ProductId, od.UnitPrice }),
        (o, a) => new { o.OrderId, a.index, a.ProductId, a.UnitPrice })
    .ToList();

上の「その 2」とほぼ同様な操作を行いますが、それに加えてオーダー毎の index を付与できるところが異なります。

このオーバーロードは SQL Server などの DB で使われる SQL に変換できないので、Linq to Entities では使えないことに注意してください。

上のコードの実行結果は以下の通りとなります。

その 4 の結果

Tags: , ,

ADO.NET

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

by WebSurfer 2023年2月28日 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 2023年2月24日 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

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar