先の記事「SelectMany メソッド」の続きです。
Microsoft のドキュメント「Enumerable.SelectMany メソッド」によるとこのメソッドには 4 つのオーバーロードがあります。それらの使い方を調べましたので、備忘録として書いておきます。
SQL Server サンプルデータベース Northwind の Orders, Order_Details テーブルから、リバースエンジニアリングで生成したコンテキスクラストとエンティティクラスをベースに使います。下の画像は Visual Studio 2022 の拡張機能 EF Core Power Tools を使って 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> を返します)。
・・・ということで、上のコードの実行結果は以下の通りとなります。
(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<匿名型>) を生成して戻り値として返しています。
上のコードの実行結果は以下の通りとなります。
上のコード例では 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 では使えないことに注意してください。
上のコードの実行結果は以下の通りとなります。
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 では使えないことに注意してください。
上のコードの実行結果は以下の通りとなります。