CodeZine(コードジン)

特集ページ一覧

N+1問題を回避せよ! LINQから出力されるSQLを見てみよう&遅延ローディングの光と闇

Visual StudioでDB連携も簡単プログラミング ~知っておきたいLINQメソッド式&ラムダ式 第3回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2015/02/03 14:00
目次

N+1問題の回避

 N+1問題の回避方法は簡単で、遅延ローディングを使わず、使用するテーブルをあらかじめ宣言することです。LINQではIncludeというメソッドで、まとめて読み込むテーブルを指定できます(リスト7)。

リスト7 Includeメソッド(ラムダ式版)で読み込むテーブルを指定
using System.Data.Entity; //ラムダ式版Includeメソッド用
・・・
//あらかじめ読み込むテーブルを指定する
products = context.Products.Where(x => x.Price < 300)
             .Include(x => x.Employee.Department).Include(x => x.Maker);

 ここでは、「Productsテーブルから関連するEmployeesテーブル(からさらにDepartmentsテーブル)とMakersテーブルまで読み込む」と指定しています。(厳密には、「テーブル名」ではなく、読み込む「プロパティ名」をラムダ式で指定しています)。

 実行結果はリスト8のように、Productsテーブルから必要な3テーブルにJOINをかけたSQLとなります。少し複雑なSQLとなり、実行時間も長くなりますが、このSQL1回だけで必要な情報を取得できますので、N+1問題は発生せず、総合的なパフォーマンスは向上することでしょう。

リスト8 JOINを使用した効率の良いSQL
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Price] AS [Price],
    [Extent2].[Id] AS [Id1],
    [Extent2].[Name] AS [Name1],
    [Extent2].[Birthday] AS [Birthday],
    [Extent3].[Id] AS [Id2],
    [Extent3].[Name] AS [Name2],
    [Extent4].[Id] AS [Id3],
    [Extent4].[Name] AS [Name3]
    FROM    [dbo].[Products] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
      ON [Extent1].[Employee_Id] = [Extent2].[Id]
    LEFT OUTER JOIN [dbo].[Departments] AS [Extent3]
      ON [Extent2].[Department_Id] = [Extent3].[Id]
    LEFT OUTER JOIN [dbo].[Makers] AS [Extent4]
      ON [Extent1].[Maker_Id] = [Extent4].[Id]
    WHERE [Extent1].[Price] < 300
-- 2014/10/26 1:55:45 +09:00
 で実行しています
-- 42 ミリ秒で完了しました。結果: SqlDataReader

 なお、Includeメソッドは複数の定義があり、読み込む対象を文字列で指定することも可能です(リスト9)。

リスト9 Includeメソッドで読み込み対象を文字列で指定
//読み込み対象を文字列で指定するInclude。これは正直イケてない……
products = context.Products.Where(x => x.Price < 300)
  .Include("Employee.Department").Include("Maker");

 しかし、関連するプロパティ名を文字列で持ってしまっては、統合言語クエリの旨味が薄れてしまいます(文字列になってしまってはIntelliSenseも効かないのです!)。ですので、前述のラムダ式で読み込むプロパティを指定するIncludeメソッドの使用を推奨します。なお、このラムダ式版Includeメソッドを使用するには、「using System.Data.Entity;」の記述が必要になりますのでご注意ください。

遅延ローディングの無効化

 このように、N+1問題が起きる箇所では遅延ローディングを使用せず、Includeメソッドで関連するテーブルを読み込んでいく、というのが基本的な対策となります。しかし「Includeメソッドを書き忘れた時に自動的に遅延ローディングが起きてしまうのはイヤだ。N+1問題を起こすぐらいなら、そもそも遅延ローディングを切っちゃいたい」という要望もあるでしょう。デフォルトでは遅延ローディングは有効ですが、リスト10のように、コンテキストクラスのConfigurationプロパティの、LazyLoadingEnabledプロパティにfalseを設定することで、遅延ローディングを無効化できます。

リスト10 遅延ローディングの無効化
//遅延ローディングを無効化
context.Configuration.LazyLoadingEnabled = false;

//遅延ローディングを無効化した状態で、うっかりIncludeを忘れた
products = context.Products.Where(x => x.Price < 300);
//DumpProductsで別テーブルのデータにアクセスする時にNullPointerException発生……
DumpProducts(products);

 当然ながら、遅延ローディングを無効化した状態で、必要なIncludeを忘れると、別テーブルのプロパティにアクセスしようとした時点でNullPointerExceptionが発生します。この辺りは諸刃の剣ですね。遅延ローディングの有効/無効化はコンテキストクラスのインスタンスごとに個別に指定できますので、必要に応じて使い分けが可能です。個人的には、遅延ローディングの恩恵は捨てがたいため、パフォーマンスが必要な箇所では遅延ローディングを無効化してIncludeで関連するテーブルを明示的に指定し、特に問題とならない箇所では遅延ローディングにお任せするようなコーディングを実践しています。遅延ローディングにはメリット・デメリットの双方がありますので、用法・容量を守ってお使いください。

注釈

 なお、遅延ローディングの無効化はコンテキストクラスを生成してすぐに(最初のSQLを発行する前に)指定しないと無視されるようです。無効化する位置にご注意ください。

まとめ

 本記事では、Entity Frameworkが発行するSQLを確認する方法と、遅延ローディングとN+1問題の微妙な関係について解説しました。幾らかLINQが脇役にそれた感はありますが、データベースプログラミングでLINQを活用する上では外せないポイントですので、しっかり抑えておきましょう。次回は「SQLに変換されるモノと変換されないモノ」と題して、どんなLINQがSQLに変換されるのか、もう少し解説します。



  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:Visual StudioでDB連携も簡単プログラミング ~知っておきたいLINQメソッド式&ラムダ式

著者プロフィール

  • WINGSプロジェクト 土井 毅(ドイ ツヨシ)

    <WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂...

  • 山田 祥寛(ヤマダ ヨシヒロ)

    静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for ASP/ASP.NET。執筆コミュニティ「WINGSプロジェクト」代表。 主な著書に「入門シリーズ(サーバサイドAjax/XM...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5