つばろぐ

主に C#, .NET, Azure の備忘録です。たまに日記。

LINQ to Entitiesに関する私の勘違いを見直した話

久しぶりにブログを書きます。

今、仕事でASP.NETを使ったWebアプリケーションの開発に取り組んでいます。
私自身、業務でWebアプリケーションを作るのは初めてなので、中々苦戦しています。
(昔のASPなら大学時代に作ってたけど・・・)

ASP.NETにはMVCの考え方が採用されています。
資料が無い状態では中々思うように開発が進まないので、『ASP.NET MVC 5 実践プログラミング』という書籍を参考にしています。

www.shuwasystem.co.jp

ASP.NETからデータベースを扱う場合、Entity Frameworkという技術を用いて、コードファーストな開発を行います。
Entity Frameworkやコードファーストについては以下を参考にしてみて下さい。

codezine.jp

codezine.jp

またタイトルにある LINQ to Entitiesとは、LINQを使ってEntity Framework経由でデータベースを操作する技術を言います。
LINQについての説明は省略しますが、C#プログラミングではLINQは必ずと言っていいほど利用します。

LINQ to Entitiesの処理に時間がかかっていると感じていた

サンプルを使って説明します。

//Bookクラス
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int Price { get; set; }
}

//DBコンテキストクラス
public class TsubalogWebContext : DbContext
{
    public DbSet<Book> Books { get; set; }
}

上記のクラスの大量データに対して検索SQLを発行します。
なお先に紹介した書籍にあるように、リポジトリパターンを使ってみます。
例として、1000円以下の書籍を抽出してみます。

// リポジトリクラス
public class TsubalogWebRepository : ITsubalogWebRepository
{
    private TsubalogWebContext db = new TsubalogWebContext();

    public IEnumerable<Book> GetAll()
    {
        return from b in db.Books select b;
    }

    public IEnumerable<Book> Filter()
    {
        return GetAll().Where(b => b.Price < 1000);
    }
}

Filter()メソッドを実行した時に、想像以上に処理時間がかかってるわけですよ。
そこでLINQ to Entitiesで発行されるSQLを確認したところ、次のようなSQLになっていました。

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Price] AS [Price]
    FROM [dbo].[Books] AS [Extent1]

SQL文のWhere句がありません。
「ちゃんと条件を指定しているのになぜ??」状態で小一時間悩みました。

このコードがどのような状態かと言うと、

  1. LINQ to Entitiesで全件取得し、
  2. LINQ to Objectsで全件データから1000円以下の書籍を検索する

ということになります。
これでは全件データがメモリに展開されてしまい、パフォーマンスが悪いことも納得です。

LINQ to Entitiesで勘違いしていたことは何か

先の問題の解決のヒントとなったのは、CodeZineの記事でした。

codezine.jp

つまり、LINQ to Entitiesを使ってクエリを発行したいデータはIQueryableインターフェイスでなければならない、ということでした。
GetAll()メソッドの戻り値の型を、IQueryable<Book>にしなければならなかったようです。

// リポジトリクラス
public class TsubalogWebRepository : ITsubalogWebRepository
{
    private TsubalogWebContext db = new TsubalogWebContext();

    public IQueryable<Book> GetAll()
    {
        return from b in db.Books select b;
    }

    public IEnumerable<Book> Filter()
    {
        return GetAll().Where(b => b.Price < 1000);
    }
}

修正したLINQ to Entitiesで再度SQLを発行してみると、きちんと条件が含まれていることが分かります。

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Price] AS [Price]
    FROM [dbo].[Books] AS [Extent1]
    WHERE [Extent1].[Price] < 1000

これまでコレクションは極力IEnumerableで扱っていた自分にとって、型をきちんと意識することが大事だと再認識した問題でした。