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

PlanetScale を使ってみました

by WebSurfer 2023年2月15日 19:11

ASP.NET Core MVC アプリで PlanetScale をデーターベースに使ってみました。紆余曲折がありましたが何とか接続して使えるようになった結果が下の画像です。

PlanetScale を使ったアプリ

その紆余曲折と言うか、アプリの作成で自分が遭遇した問題は以下の通りです。

  1. EF Code First でテーブルを作成できない。
  2. 既存の DB からリバースエンジニアリングができない。
  3. BeginTransaction メソッドが使えない。

その顛末を以下に備忘録として書いておきます。上の各項目についても説明します。

PlanetScale とは何かですが、"PlanetScale is the world’s most advanced serverless MySQL platform" ということだそうで、要するに MySQL を使ったクラウド上のデータベースサーバーらしいです。制約として外部キー制約がつけられないということがあるそうです。

利用するにあたって、まず、PlanetScale にサインインしてデータベース名と地域を設定し、データベースを作成する必要があります。以下の画像がその設定の例です。

データベース名と地域を設定

データベースの作成に成功すると、以下のように自分が作成したデーターベース情報が表示されます。

自分の DB 情報

上の画像の[Connect]ボタンをクリックすると、下の画像のように接続情報が表示されます。Username と Password が表示されるのは初回のみなので必ず記録しておいてください

接続情報

さらに、上の画像の赤枠の[.NET]を選択すると ASP.NET Core アプリの appsettings.json に設定するための接続文字列の例が表示されますので、これも忘れないように記録しておくことをお勧めします。

以上はデータベース作成時からデフォルトで存在している main ブランチでの話です。このあと開発作業用にブランチを追加するのが普通ということなので自分もそうしてみました。気をつけなければならないのは追加した開発作業用ブランチへの接続用の Username と Password は main ブランチのものとは異なることです。

自分はそれを知らなくて半日ぐらいハマりました。開発作業用ブランチを作ってそれにテーブルを作成し、アプリからアクセスして SELECT クエリを投げたのですが、MySqlException がスローされてそのテーブルは見つからないと言われます。原因は Username と Password に main ブランチのものを使っていたので main ブランチに接続され、main ブランチにはそのテーブルは存在しないからでした。開発作業用ブランチに接続するには、開発作業用ブランチの Username と Password を使用する必要があるようです。

以上で PlanetScale 側のデータベースの準備は完了したはずなので、Visual Studio 2022 のテンプレートを使って .NET 6.0 の ASP.NET Core MVC のプロジェクトを新規に作成し、それから PlanetScale を使ってみます。

(1) EF Code First でテーブルを作成できない

最初に EF Code First の機能を使って PlanetScale にアプリが使うテーブルを作成してみます。先の記事「MySQL で Movie チュートリアル (CORE)」に書いた手順で、EF Code First の機能を利用して PlanetScale に Movie テーブルの作成をトライしてみました。

Add-Migration では問題なく Migration のためのクラスファイルが作成されます。しかし、Update-Database でのテーブルの作成に失敗します。

プロバイダに MySql.EntityFrameworkCore 6.0.10 を使った場合は、Update-Database の実行で KeyNotFoundException がスローされて失敗します。

Pomelo.EntityFrameworkCore.MySql 6.0.2 を使った場合は、ALTER DATABASE CHARACTER SET utf8mb4 というコマンドで失敗し "alter database is not supported" というエラーメッセージが出て、テーブルの作成に失敗します。

.NET 7.0 でも試してみましたが同じエラーで失敗します。

(2) リバースエンジニアリングができない

解決方法が分からないので、EF Code First による Movie テーブルの作成は諦めて、PlanetScale のサイトにアクセスして Console から手動で Movie テーブルを作成しました。下の画像の movie というのがそれです。

Console から手動で Movie テーブル作成

(注: __EFMigrationsHistory というテーブルもありますが、それは上に書いた EF Code First の操作でできたものです。その時は肝心の Movie テーブルは作成されていません)

先に作ったプロジェクトは放置して新たにゼロから ASP.NET Core MVC のプロジェクトを作成し、プロバイダには Pomelo.EntityFrameworkCore.MySql 6.0.2 を使って、リバースエンジニアリングでコンテキストクラスとエンティティクラスを作成をトライしました。

エラーなしで完了するもののコンテキストクラスしか生成されずその内容も不完全です。エンティティクラスは全く作成されません。

ちなみに、プロバイダに MySql.EntityFrameworkCore 6.0.10 を使った場合は KeyNotFoundException がスローされて何も生成されずに終わってしまいます。

これでは先に進めないので、不完全なコンテキストクラスは手直しして、エンティティクラスは自分でコードを書いて追加しました。その内容は以下の通りです。

コンテキストクラス

using Microsoft.EntityFrameworkCore;
using PlanetScaleMovie2.Models;

namespace PlanetScaleMovie2.Data
{
    public partial class myplanetscaleContext : DbContext
    {
        public myplanetscaleContext()
        {
        }

        public myplanetscaleContext(DbContextOptions<myplanetscaleContext> options)
            : base(options)
        {
        }

        // 以下の一行だけは自動生成されないので手動で追加
        public virtual DbSet<Movie> Movies { get; set; } = null!;

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // 自動生成されたコードは不要なので削除
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.UseCollation("utf8mb4_0900_ai_ci")
                .HasCharSet("utf8mb4");

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

エンティティクラス

using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace PlanetScaleMovie2.Models
{
    [Table("movie")]
    public partial class Movie
    {
        [Key]
        public int Id { get; set; }
        [Column(TypeName = "text")]
        public string? Title { get; set; }
        [Column(TypeName = "datetime")]
        public DateTime ReleaseDate { get; set; }
        [Column(TypeName = "text")]
        public string? Genre { get; set; }
        public decimal Price { get; set; }
    }
}

そのあとスキャフォールディング機能を使って Controller と View を作成します。

Controller と View の作成

Program.cs でコンテキストクラスを DI コンテナに登録します。

var serverVersion = new MySqlServerVersion(new Version(8, 0, 32));
var coonecctionString = builder.Configuration.GetConnectionString("PlanetScaleMovieContext");
builder.Services.AddDbContext<myplanetscaleContext>(options =>
    options.UseMySql(coonecctionString, serverVersion));

appsettings.json に上のコードの "PlanetScaleMovieContext" という名前で接続文字列を追加します。Movie テーブル を開発作業用ブランチに作った場合は、接続文字列の user と password は開発作業用ブランチ用のものにすることに注意してください。

以上でアプリは今度は問題なく動きました。その結果がこの記事の一番上の画像です。

(3) BeginTransaction メソッドが使えない

これはプロバイダに Oracle 製の MySql.EntityFrameworkCore を使った場合です。以下のように BeginTransaction メソッドの行で KeyNotFoundException がスローされます。

BeginTransaction メソッド

プロバイダに Pomelo.EntityFrameworkCore.MySql を使った場合は問題ありません。Oracle 製はどうも相性が良くないようです。

なお、上記 (1), (2) の件については planetscale / discussionDoes PlanetScale support the .NET 6/7 EF Code First and reverse engineering? という質問を出しました。解決に向けて進展がありましたらこの記事に追記します。

Tags: , , ,

CORE

SelectMany メソッド

by WebSurfer 2022年3月21日 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

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

View posts in large calendar