Entity Framework コードファーストで必要な項目のみを取得する

エンティティフレームワーク コードファーストは便利ですが、一覧を取得する際に SELECT * FROM DbTable という全項目取得な SQL が発行されてしまいます。これが気になっていて、ちょっとアイデアが浮かんだので試してみたところ、うまく行ったので書いてみます。

アイデアというのは、「なぜ SELECT * な SQL の発行になるのかな」というところで、「LINQ to Entity のクエリから SQL を組み立てるところで、取得する項目を限定するための情報がないからじゃ?」と考え、「それなら必要な項目を明示するようにしてみたらいいんじゃない」というものです。

具体的には、LINQ to Entity なクエリ中に射影を組み込んで、必要な項目を明示するというものです。射影で作成する型情報を MVC のビュー側で参照する必要があるので、Models フォルダ下に Views フォルダを作成し、当該フォルダに DB のビュー的なクラスを作成します。

コードに行く前に、発行される SQL を確認するための方法を。
TextWriter クラスを継承する LogWriter クラスを作成し、Application_Start() メソッドの中で Console.SetOut(new LogWriter()); と記述しておきます。あとはリポジトリのコンストラクタで _context.Database.Log = Console.Write; と記述しておけば、Visual Studio のデバッグ出力のウィンドウにログが出力されます(このアイデアはStack Overflow に書かれていたものです)。ログ出力には log4net を使う手もありますがチョット確認したいだけなので、こちらのアイデアのほうが簡便です 😉

public class LogWriter : System.IO.TextWriter
{
    public override System.Text.Encoding Encoding
    {
        get { return System.Text.Encoding.UTF8; }
    }

    public override void Write(string value)
    {
        System.Diagnostics.Debug.Write(value);
    }
}

動作確認は、本の情報を表示するプログラムで行います。

それでは、動作確認のためのコードです。

「新しいプロジェクト」で「ASP.NET Web アプリケーション」を選択し、「名前」に「RetrievingIndividualItem」、「場所」にプロジェクトを保存するフォルダを設定します。

テンプレートから「MVC」を選択し、「認証の変更」ボタンをクリックして「認証なし」を選択して「OK」ボタンをクリック、Windows Azure のチェックボックスのチェックを外して、「OK」ボタンをクリックします。

確認には、SQL Server Compact とエンティティ フレームワーク コードファーストを利用するので、必要なパッケージを NuGet を利用して取得します。
メニューの「プロジェクト」から「NuGet パッケージの管理…」を選択し、次のパッケージをインストールします。

  • EntityFramework
  • EntityFramework.SqlServerCompact

モデルを作成します。
Models フォルダに Book クラスを作成します。

using System;

namespace RetrievingIndividualItem.Models
{
    public class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public decimal Price { get; set; }
        public string Summary { get; set; }
        public DateTime Published { get; set; }
        public int TotalPages { get; set; }

        public Publisher Publisher { get; set; }
        public Writer Writer { get; set; }
    }
}

次に Publisher クラスを作成します。

using System.Collections.Generic;

namespace RetrievingIndividualItem.Models
{
    public class Publisher
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string PhoneNumber { get; set; }
        public string Address { get; set; }

        public ICollection<Book> Books { get; set; }
    }
}

次に Writer クラスを作成します。

using System.Collections.Generic;

namespace RetrievingIndividualItem.Models
{
    public class Writer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
        public string Address { get; set; }

        public ICollection<Book> Books { get; set; }
    }
}

次に、DB のビュー的なクラスを作成します(クエリと MVC のビューで利用するものです。)。
Models フォルダ下に Views フォルダを作成します。
Views フォルダに BookListView クラスを作成します。

using System;
using System.ComponentModel.DataAnnotations;

namespace RetrievingIndividualItem.Models.Views
{
    public class BookListView
    {
        public int Id { get; set; }
        [Display(Name = "タイトル")]
        public string Title { get; set; }
        [Display(Name = "出版社")]
        public string PublisherName { get; set; }
        [Display(Name = "著者")]
        public string WriterName { get; set; }
        [Display(Name = "出版日")]
        public DateTime Published { get; set; }
    }
}

次にコンテキストやリポジトリ等を作成します。
プロジェクトに DAL フォルダを作成します。
DAL フォルダに BooksContext クラスを作成します。

using System.Data.Entity;

using RetrievingIndividualItem.Models;

namespace RetrievingIndividualItem.DAL
{
    public class BooksContext : DbContext
    {
        public BooksContext() : base("BooksDb") { }

        public DbSet<Book> Books { get; set; }
        public DbSet<Publisher> Publishers { get; set; }
        public DbSet<Writer> Writers { get; set; }
    }
}

次に初期データ投入の CustomSeedInitializer クラスを作成します。このプログラムは動作確認用のものなので、マイグレーション機能は利用しません。

using System;
using System.Collections.Generic;
using System.Data.Entity;

using RetrievingIndividualItem.Models;

namespace RetrievingIndividualItem.DAL
{
    public class CustomSeedInitializer : DropCreateDatabaseIfModelChanges<BooksContext>
    {
        protected override void Seed(BooksContext context)
        {
            base.Seed(context);

            var pub1 = context.Publishers.Add(new Publisher { Name = "獣出版", PhoneNumber = "123", Address = "獣のすみか" });
            var pub2 = context.Publishers.Add(new Publisher { Name = "鳥出版", PhoneNumber = "456", Address = "鳥のすみか" });
            var pub3 = context.Publishers.Add(new Publisher { Name = "魚出版", PhoneNumber = "789", Address = "海の中" });

            var wtr10 = context.Writers.Add(new Writer { Name = "犬", PhoneNumber = "123-123", Email = "inu@example.com", Address = "獣のすみか" });
            var wtr11 = context.Writers.Add(new Writer { Name = "猫", PhoneNumber = "123-456", Email = "neko@example.com", Address = "獣のすみか" });
            var wtr20 = context.Writers.Add(new Writer { Name = "スズメ", PhoneNumber = "456-123", Email = "suzume@example.com", Address = "鳥のすみか" });
            var wtr21 = context.Writers.Add(new Writer { Name = "シジュウカラ", PhoneNumber = "456-456", Email = "sijukara@example.com", Address = "鳥のすみか" });
            var wtr30 = context.Writers.Add(new Writer { Name = "イワシ", PhoneNumber = "789-123", Email = "iwashi@example.com", Address = "海の中" });
            var wtr31 = context.Writers.Add(new Writer { Name = "アジ", PhoneNumber = "789-456", Email = "aji@example.com", Address = "海の中" });

            new List<Book> {
                new Book { Title = "犬の散歩", Price = 900, Published = DateTime.Parse("1990/1/1"), Summary = "犬の散歩について", TotalPages = 100, Publisher = pub1, Writer = wtr10 },
                new Book { Title = "雪と犬", Price = 800, Published = DateTime.Parse("1991/1/1"), Summary = "雪と犬について", TotalPages = 80, Publisher = pub1, Writer = wtr10 },
                new Book { Title = "骨と犬", Price = 800, Published = DateTime.Parse("1992/1/1"), Summary = "骨と犬について", TotalPages = 80, Publisher = pub1, Writer = wtr10 },
                new Book { Title = "猫の散歩", Price = 900, Published = DateTime.Parse("1993/1/1"), Summary = "猫の散歩について", TotalPages = 100, Publisher = pub1, Writer = wtr11 },
                new Book { Title = "こたつと猫", Price = 800, Published = DateTime.Parse("1994/1/1"), Summary = "こたつと猫について", TotalPages = 80, Publisher = pub1, Writer = wtr11 },
                new Book { Title = "猫じゃらしと猫", Price = 800, Published = DateTime.Parse("1995/1/1"), Summary = "猫じゃらしと猫について", TotalPages = 80, Publisher = pub1, Writer = wtr11 },
                new Book { Title = "スズメの会話", Price = 900, Published = DateTime.Parse("1996/1/1"), Summary = "スズメのさえずりについて", TotalPages = 100, Publisher = pub2, Writer = wtr20 },
                new Book { Title = "電線とスズメ", Price = 800, Published = DateTime.Parse("1997/1/1"), Summary = "電線とスズメについて", TotalPages = 80, Publisher = pub2, Writer = wtr20 },
                new Book { Title = "種とスズメ", Price = 800, Published = DateTime.Parse("1998/1/1"), Summary = "種とスズメについて", TotalPages = 80, Publisher = pub2, Writer = wtr20 },
                new Book { Title = "シジュウカラの会話", Price = 900, Published = DateTime.Parse("1999/1/1"), Summary = "シジュウカラのさえずりについて", TotalPages = 100, Publisher = pub2, Writer = wtr21 },
                new Book { Title = "電線とシジュウカラ", Price = 800, Published = DateTime.Parse("2000/1/1"), Summary = "電線とシジュウカラについて", TotalPages = 80, Publisher = pub2, Writer = wtr21 },
                new Book { Title = "種とシジュウカラ", Price = 800, Published = DateTime.Parse("2001/1/1"), Summary = "種とシジュウカラについて", TotalPages = 80, Publisher = pub2, Writer = wtr21 },
                new Book { Title = "イワシのすみか", Price = 900, Published = DateTime.Parse("2002/1/1"), Summary = "イワシのすみかについて", TotalPages = 100, Publisher = pub3, Writer = wtr30 },
                new Book { Title = "イワシの集団", Price = 900, Published = DateTime.Parse("2003/1/1"), Summary = "イワシの集団について", TotalPages = 100, Publisher = pub3, Writer = wtr30 },
                new Book { Title = "プランクトンとイワシ", Price = 800, Published = DateTime.Parse("2004/1/1"), Summary = "プランクトンとイワシについて", TotalPages = 80, Publisher = pub3, Writer = wtr30 },
                new Book { Title = "アジのすみか", Price = 900, Published = DateTime.Parse("2005/1/1"), Summary = "アジのすみかについて", TotalPages = 100, Publisher = pub3, Writer = wtr31 },
                new Book { Title = "アジの集団", Price = 900, Published = DateTime.Parse("2006/1/1"), Summary = "アジの集団について", TotalPages = 100, Publisher = pub3, Writer = wtr31 },
                new Book { Title = "小魚とイワシ", Price = 800, Published = DateTime.Parse("2007/1/1"), Summary = "小魚とアジについて", TotalPages = 80, Publisher = pub3, Writer = wtr31 },
            }.ForEach(n => context.Books.Add(n));

            context.SaveChanges();
        }
    }
}

次に IBookRepository インターフェイスを作成します。

using System;
using System.Linq;

using RetrievingIndividualItem.Models;
using RetrievingIndividualItem.Models.Views;

namespace RetrievingIndividualItem.DAL
{
    public interface IBookRepository : IDisposable
    {
        IQueryable<Book> FindBooks();

        IQueryable<BookListView> FindBookListView();
    }
}

見て分かるとおり、一覧用にモデルを返すものとビュー的なものを返すものの二種類のメソッドを定めています。

次に BookRepository クラスを作成します。

using System;
using System.Data.Entity;
using System.Linq;

using RetrievingIndividualItem.Models;
using RetrievingIndividualItem.Models.Views;

namespace RetrievingIndividualItem.DAL
{
    public class BookRepository : IBookRepository
    {
        private BooksContext _context;

        public BookRepository()
        {
            _context = new BooksContext();

            // SQL のログ取得の設定
            // Console.Write のデバッグ ウィンドウへの出力設定は Global.asax.cs で行っている
            // log4net を使う手もあるがチョット確認したいだけなので
            _context.Database.Log = Console.Write;
        }

        #region IBookRepository メンバー

        public IQueryable<Book> FindBooks()
        {
            return _context.Books
                .AsNoTracking() // Context 内にトラッキングしない
                .Include(p => p.Publisher) // Eager Loading の指定
                .Include(p => p.Writer);   // Eager Loading の指定
        }

        public IQueryable<BookListView> FindBookListView()
        {
            return _context.Books
                .AsNoTracking() // Context 内にトラッキングしない
                .Include(p => p.Publisher) // Eager Loading の指定
                .Include(p => p.Writer)    // Eager Loading の指定
                .Select(s => new BookListView // 射影で BookListBiew のインスタンスを作成
                {
                    Id = s.Id,
                    Title = s.Title,
                    Published = s.Published,
                    PublisherName = s.Publisher.Name,
                    WriterName = s.Writer.Name
                });
        }

        #endregion

        #region IDisposable メンバー

        private bool _disposed = false;

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// リソースの開放を行います。
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _context.Dispose();
        }

        #endregion

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~BookRepository()
        {
            Dispose(false);
        }
    }
}

コンストラクタで SQL のログ取得の設定を行っています。

AsNoTracking, Include 指定については、でんさんラボ さんの 4.3.4. 大量データ読み出し時の注意5.3. リレーションデータの読み込み の記事が参考になります。

FindBookListView メソッドのほうで、射影により BookListView クラスのインスタンスを生成するようにしています。この LINQ to Entity なクエリで DB から取得する必要がある項目(射影で利用する項目)を明示しています。
ちなみに、戻り値を IQueryable 型としていますが、ToList() として戻り値を IList 型にしたときとの違いは、遅延実行の有無だけです。必要なのは、射影を行うことで必要な項目を LINQ to Entity のクエリ中で明らかにするということです。

次に接続文字列の設定です。
プロジェクトのルートにある Web.config ファイルの configuration 要素の子要素 configSections の後に connectionStrings 要素を追加します。

<configuration>
  ~
  </configSections>
  <connectionStrings>
    <add name="BooksDb" connectionString="Data Source=|DataDirectory|\BooksDb.sdf" providerName="System.Data.SqlServerCe.4.0" />
  </connectionStrings>
  <appSettings>
  ~

次に HomeController を修正します。

using System.Web.Mvc;

using RetrievingIndividualItem.DAL;

namespace RetrievingIndividualItem.Controllers
{
    public class HomeController : Controller
    {
        private readonly IBookRepository _repository;

        public HomeController() : this(new BookRepository()) { }
        public HomeController(IBookRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            return View(_repository.FindBooks());
        }

        public ActionResult Index2()
        {
            return View(_repository.FindBookListView());
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }

        protected override void Dispose(bool disposing)
        {
            _repository.Dispose();

            base.Dispose(disposing);
        }
    }
}

Index アクションが通常のもの、Index2 アクションが射影を追加しているクエリを利用するものです。

次にビューです。
Home の Index ビューを再生成します。
index アクションを右クリックして「ビューの追加」を選択
テンプレートに List を選択
モデル クラスに Book を選択
データ コンテキスト クラスは空欄
「追加」ボタンをクリックし、置き換えの確認で「はい」をクリック

ビューの生成後、末尾に Index2 へのリンクの設定を追加します。


~
</table>

<p>
    @Html.ActionLink("Index2", "Index2")
</p>

同様に Index2 のビューを生成します。
テンプレートに List を選択
モデル クラスに BookListView を選択
データ コンテキスト クラスは空欄
「追加」ボタンをクリック

最後に Global.asax.cs ファイルです。
DB の初期データ投入の設定と SQL のログ取得の設定を行います。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

using RetrievingIndividualItem.DAL;

namespace RetrievingIndividualItem
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // DB の初期データ投入の設定
            Database.SetInitializer(new CustomSeedInitializer());

            // このアイデアは stackoverflow から
            // http://stackoverflow.com/questions/9614218/how-to-use-console-writeline-in-asp-net-c-during-debug
            Console.SetOut(new LogWriter());
        }
    }

    public class LogWriter : System.IO.TextWriter
    {

        public override System.Text.Encoding Encoding
        {
            get { return System.Text.Encoding.UTF8; }
        }

        public override void Write(string value)
        {
            System.Diagnostics.Debug.Write(value);
        }
    }
}

コードは以上です。

動かして発行された SQL は次のとおりです。
モデルを返すクエリのもの。

Opened connection at 2014/07/05 23:37:40 +09:00
SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Summary] AS [Summary], 
    [Extent1].[Published] AS [Published], 
    [Extent1].[TotalPages] AS [TotalPages], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[Name] AS [Name], 
    [Extent2].[PhoneNumber] AS [PhoneNumber], 
    [Extent2].[Address] AS [Address], 
    [Extent3].[Id] AS [Id2], 
    [Extent3].[Name] AS [Name1], 
    [Extent3].[PhoneNumber] AS [PhoneNumber1], 
    [Extent3].[Email] AS [Email], 
    [Extent3].[Address] AS [Address1]
    FROM   [Books] AS [Extent1]
    LEFT OUTER JOIN [Publishers] AS [Extent2] ON [Extent1].[Publisher_Id] = [Extent2].[Id]
    LEFT OUTER JOIN [Writers] AS [Extent3] ON [Extent1].[Writer_Id] = [Extent3].[Id]
-- Executing at 2014/07/05 23:37:40 +09:00
-- Completed in 3 ms with result: SqlCeDataReader

Closed connection at 2014/07/05 23:37:41 +09:00

射影を行ったクエリのもの。

Opened connection at 2014/07/05 23:40:39 +09:00
SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Published] AS [Published], 
    [Extent2].[Name] AS [Name], 
    [Extent3].[Name] AS [Name1]
    FROM   [Books] AS [Extent1]
    LEFT OUTER JOIN [Publishers] AS [Extent2] ON [Extent1].[Publisher_Id] = [Extent2].[Id]
    LEFT OUTER JOIN [Writers] AS [Extent3] ON [Extent1].[Writer_Id] = [Extent3].[Id]
-- Executing at 2014/07/05 23:40:39 +09:00
-- Completed in 2 ms with result: SqlCeDataReader

Closed connection at 2014/07/05 23:40:39 +09:00

結果のとおり、必要な項目のみを取得する SQL が発行されています 😎


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です