5

コードを簡潔に保つために、LINQ to SQL のデータ アクセス コードの一部を、昔ながらのビジネス ロジック コードと同様に、プライベート サブメソッドに分解しようとすることがよくあります。非常に単純な例を挙げましょう。

public IEnumerable<Item> GetItemsFromRepository()
{
    var setA = from a in this.dataContext.TableA
               where /* criteria */
               select a.Prop;

    return DoSubquery(setA);
}

private IEnumerable<Item> DoSubQuery(IEnumerable<DateTimeOffset> set)
{
     return from item in set
            where /* criteria */
            select item;
}

ネストが深くなったり、セットの結果を使用して他のクエリをフィルタリングしたりする、より複雑な例を想像することで、誰も想像力を膨らませることはないと確信しています。

私の基本的な質問は次のとおりです。プライベート メソッドで LINQ to SQL コードを再編成するだけで、いくつかの重大なパフォーマンスの違いや例外がスローされることさえ見てきました。効率的でクリーンなデータ アクセス コードを記述する方法について十分な情報に基づいて決定できるように、これらの動作のルールを説明できる人はいますか?

私が持っていたいくつかの質問:

1) System.Linq.Table インスタンスをメソッドに渡すと、クエリが実行されるのはいつですか?

2) 別のクエリで System.Linq.Table を使用すると、いつ実行されますか?

3) パラメータをメソッドに渡した System.Linq.Table に適用できる操作の種類 (Take、First、Last、order by など) に制限はありますか?

4

2 に答える 2

5

LINQ-to-SQL に関する最も重要なルールは次のとおりですIEnumerable<T>。セマンティックが不明確であるため、必要でない限り返さないでください。それを超えた 2 つの考え方があります。

  • を返す場合IQueryable<T>構成可能です。つまり、where後のクエリを組み合わせて単一の TSQL を作成しますが、欠点として、完全にテストするのは困難です。
  • それ以外の場合は returnList<T>または同様であるため、そのポイントを超えるすべてが LINQ-to-Objects であることは明らかです

現在、あなたは途中で何かをしています: LINQ-to-Objects に ( 経由でIEnumerable<T>) 折りたたんでいますが、それは明らかではありません - 途中で接続を開いたままにしています (繰り返しますが、明らかではないため、問題のみです)。

于 2012-08-13T18:04:43.700 に答える
3

暗黙のキャストを削除します。

public IQueryable<Item> GetItemsFromRepository()
{
    var setA = from a in this.dataContext.TableA
               where /* criteria */
               select a.Prop;

    return DoSubquery(setA);
}

private IQueryable<Item> DoSubQuery(IQueryable<DateTimeOffset> set)
{
     return from item in set
            where /* criteria */
            select item;
}

IQueryable<Item>from からto への暗黙のキャストIEnumerable<Item>は、基本的AsEnumerable()にはIQueryable<Item>. もちろん、それが必要な場合もありますが、デフォルトのままにしておく必要があります。これにより、メモリ内で実行される残りの部分IQueryableだけではなく、データベースに対してクエリ全体を実行できるようになります。GetItemsFromRepository()

二次的な質問:

1) System.Linq.Table インスタンスをメソッドに渡すと、クエリが実行されるのはいつですか?

Max()、などの最終結果が必要な場合ToList()、クエリ可能なオブジェクトでも、ロードされたままの列挙型でもありません。

ただし、AsEnumerable()クエリの実行は発生しませんがAsEnumerable()、ソース データソースに対して が実行される前にのみ実行が発生した場合、オンデマンドのインメモリ データソースが生成され、それに対して残りが実行されることを意味します。 .

2) 別のクエリで System.Linq.Table を使用すると、いつ実行されますか?

上と同じ。Table<T>実装しIQueryable<T>ます。たとえば、それらのうちの 2 つを結合しても、まだ何も実行されません。

3) パラメータをメソッドに渡した System.Linq.Table に適用できる操作の種類 (Take、First、Last、order by など) に制限はありますか?

によって定義されるものIQueryable<T>

編集: と の相違点と類似点に関するいくつかのIEnumerable説明IQueryable

でできることは何でも で実行できIQueryableIEnumerableその逆も同様ですが、実行方法は異なります。

どの実装も linq クエリで使用でき、、IQueryableなどの linqy 拡張メソッドがすべて含まれます。Take()Select()GroupBy

これがどのように行われるかは、実装によって異なります。たとえばSystem.Linq.Data.Table、クエリを SQL クエリに変換することでこれらのメソッドを実装し、その結果をロード時にオブジェクトに変換します。mySourceがテーブルの場合:

var filtered = from item in mySource
  where item.ID < 23
  select new{item.ID, item.Name};

foreach(var i in filtered)
  Console.WriteLine(i.Name);

次のような SQL に変換されます。

select id, name from mySourceTable where id < 23

そして、それから列挙子が作成され、MoveNext()別の行への呼び出しごとに結果から読み取られ、そこから新しい匿名オブジェクトが作成されます。

一方、mySourcewhere aListまたは a HashSet、または実装するIEnumerable<T>が独自のクエリ エンジンを持たないその他の場合、linq-to-objects コードはそれを次のように変換します。

foreach(var item in mySource)
  if(item.ID < 23)
    yield return new {item.ID, item.Name};

これは、そのコードをメモリ内で実行できるのとほぼ同じくらい効率的です。結果は同じですが、取得方法が異なります。

ここで、すべてIQueryable<T>を同等のものに変換できるためIEnumerable<T>、必要に応じて、最初のものmySource(データベースで実行が行われる場所) を取得し、代わりに次のことを行うことができます。

var filtered = from item in mySource.AsEnumerable()
  where item.ID < 23
  select new{item.ID, item.Name};

ここでは、結果を反復処理するか、これらの結果をすべて調べる何かを呼び出すまで、データベースに対して何も実行されませんが、一度実行すると、実行を 2 つの別々のステップに分割したようになります。

var asEnum = mySource.AsEnumerable();
var filtered = from item in asEnum
  where item.ID < 23
  select new{item.ID, item.Name};

最初の行の実装は SQL を実行することSELECT * FROM mySourceTableであり、残りの実行は上記の linq-to-objects の例のようになります。

データベースに ID が 23 未満のアイテムが 10 個、ID が 23 より大きいアイテムが 50,000 個含まれている場合、パフォーマンスが大幅に低下することは容易に理解できます。

明示的なメソッドを提供するだけでなくAsEnumerable()、すべてIQueryable<T>を暗黙的に にキャストできますIEnumerable<T>。これにより、それらを処理し、foreachを処理する他の既存のコードでそれらを使用できますIEnumerable<T>が、誤って不適切なタイミングで実行すると、クエリが大幅に遅くなる可能性がありDoSubQueryますIEnumerable<DateTimeOffset>IEnumerable<Item>; _ AsEnumerable()yourIQueryable<DateTimeOffset>と yourを暗黙的に呼び出しIQueryable<Item>、データベースで実行できたことがメモリ内で実行されるようにしました。

このため、99% の確率で、IQueryable最後の瞬間まで取引を続けたいと考えています。

ただし、反対の例として、それとAsEnumerable()キャストIEnumerable<T>が狂気から存在しないことを指摘するために、2 つのことを考慮する必要があります。1 つ目は、IEnumerable<T>お互いを知らない 2 つの完全に異なるソース (たとえば、2 つの異なるデータベース、データベースと XML ファイルなど) を結合するなど、他の方法ではできないことを実行できるようにすることです。

もう1つは、IEnumerable<T>実際にはより効率的である場合もあるということです。検討:

IQueryable<IGrouping<string, int>> groupingQuery = from item in mySource select item.ID group by item.Name;
var list1 = groupingQuery.Select(grp => new {Name=grp.Key, Count=grp.Count()}).ToList();//fine
foreach(var grp in groupingQuery)//disaster!
  Console.WriteLine(grp.Count());

これは、何らかのグループ化を行うクエリ可能として設定されていますgroupingQueryが、とにかく実行されていません。list1 を作成するときは、最初にそれにIQueryable基づいて新しいものを作成します。クエリ エンジンは、最適な SQL を見つけ出し、次のようなものを考え出します。

select name, count(id) from mySourceTable group by name

これはかなり効率的に実行されます。次に、行がオブジェクトに変換され、リストに入れられます。

一方、2 番目のクエリでは、group byグループ化されていないすべてのアイテムに対して集計メソッドを実行しない の SQL 変換は自然ではありません。したがって、クエリ エンジンが思いつく最善の方法は、最初に行う:

select distinct name from mySourceTable,

そして、受け取ったすべての名前に対して、次のことを行います。

select id from mySourceTable where name = '{name found in last query goes here}'

など、これは 2 つの SQL クエリ、つまり 200,000 を意味するはずです。

この場合、最初にテーブル全体をメモリに取り込む方が効率的であるmySource.AsEnumerable()ため、はるかにうまく処理できます。(データベースから必要な列のみを取得し、その時点でメモリ内に切り替えるため、作業を行う方がさらに良いでしょう)。mySource.Select(item => new {item.ID, item.Name}).AsEnumerable()

IQueryable<T>最後の部分は覚えておく価値があります。これは、できるだけ長く一緒にいるべきであるという私たちのルールに反するからです。あまり心配する必要はありませんが、グループ化を行ってクエリが非常に遅い場合は、注意する価値があります。

于 2012-08-13T18:03:26.030 に答える