May 2016

Volume 31 Number 5

データ ポイント - Dapper、Entity Framework、およびハイブリッド アプリ

Julie Lerman

Julie Lerman当コラムでは、Entity Framework という Microsoft のオブジェクト リレーショナル マッパー (ORM) について数多く取り上げてきました。Entity Framework は、2008 年以降最高の .NET データ アクセス API と称されています。.NET の ORM は他にもありますが、パフォーマンスに優れていることから、micro-ORM という特定のカテゴリに大きな注目が集まっています。中でもよく耳にする micro-ORM が Dapper です。最近少し時間をかけて Dapper について調べてみましたが、最終的にはさまざまな開発者が、「EF と Dapper のハイブリッド ソリューションを作成し、1 つのアプリケーション内でそれぞれの ORM に最適な動作をさせることができた」と報告していることに興味を惹かれました。

多くの記事やブログ投稿を読み、開発者たちと話し、少し Dapper を使ってみました。その中で見つけたことを、Dapper を耳にしたことはあっても、実際にその内容、機能、そして好まれている理由がよくわからないという開発者にお知らせしようと考えました。筆者は専門家ではありません。むしろ、ほんの束の間興味を満たせる程度の知識しかないので、読者の皆さんが関心を持ち、さらに深く調べてくださることを願っています。

Dapper が使われる理由

Dapper には興味深い歴史があり、非常に馴染み深いリソースから生み出されました。Marc Gravell と Sam Saffron は Stack Overflow で働きながら Dapper を作成し、プラットフォームが抱えていたパフォーマンスの問題を解決しました。Stack Overflow は深刻なほどトラフィックが多いサイトで、当然の如くパフォーマンスの問題が生じていました。「Stack Exchange About」ページによると、Stack Overflow は 2015 年に 57 億ページ ビューがあったといいます。2011 年、Saffron は Gravell との共同の取り組みについて、「How I Learned to Stop Worrying and Write My Own ORM」(警告を止めて独自の ORM を作成するために学んだ方法、aka.ms/Vqpql6、英語) というブログ記事を投稿し、Stack Overflow が当時抱えていたパフォーマンスの問題は、LINQ to SQL に原因があったことを説明しました。そして、Stack Overflow でのデータ アクセスを最適化するための答えが独自の ORM として Dapper を作成することだと述べています。それから 5 年後の現在、Dapper は広く使われるようになり、オープン ソースになっています。Gravell、Stack Overflow、およびチーム メンバーの Nick Craver は、今でも github.com/StackExchange/dapper-dot-net (英語) でこのプロジェクトの管理を続けています。

Dapper の概要

Dapper は、開発者の SQL のスキルを鍛え、クエリやコマンドを思い通りに構築できるようにすることに重点を置いています。標準の ORM よりも「素材」に近く、LINQ to EF から SQL にするなど、クエリを解釈する手間をなくします。Dapper は、渡されたリストを WHERE IN 句に作り直す機能など、優れた変換機能をいくつか備えています。しかし、ほとんどの場合、Dapper に送った SQL はすぐに実行でき、クエリはデータベースに対して極めて高速に動作します。SQL が得意なら、最もパフォーマンスに優れている可能性のあるコマンドを作成していると確信できます。クエリを実行するには、既知の接続文字列を指定した SqlConnection など、何らかの型の IDbConnection を作成する必要があります。その後、Dapper は自身の API によってクエリを実行し (クエリ結果のスキーマが対象とする型のプロパティに一致する場合)、オブジェクトのインスタンスを自動的に作成して、クエリ結果をオブジェクトに設定します。パフォーマンスの顕著なメリットがもう 1 つあります。Dapper は学習したマッピングを効率よくキャッシュするため、その後のクエリのシリアル化解除が極めて高速になります。今回設定するクラス DapperDesigner (図 1 参照) は、とても洗練された洋服を作るデザイナーを管理するために定義しています。

図 1 DapperDesigner クラス

public class DapperDesigner
{
  public DapperDesigner() {
    Products = new List<Product>();
    Clients = new List<Client>();
  }
  public int Id { get; set; }
  public string LabelName { get; set; }
  public string Founder { get; set; }
  public Dapperness Dapperness { get; set; }
  public List<Client> Clients { get; set; }
  public List<Product> Products { get; set; }
  public ContactInfo ContactInfo { get; set; }
}

クエリの実行場所となるプロジェクトには、NuGet から取得した (インストールパッケージの) Dapper への参照があります。以下のコードは、DapperDesigners テーブルのすべての行に対してクエリを実行するための、Dapper からの呼び出しのサンプルです。

var designers = sqlConn.Query<DapperDesigner>("select * from DapperDesigners");

今回のコード リストでは、テーブルのすべての列が必要なとき、クエリの列を明示的にプロジェクションするのではなく、select * を使用しています。sqlConn は、既にインスタンスを作成し、接続文字列を含む既存の SqlConnection オブジェクトですが、まだ開いてはいません。

Query メソッドは、Dapper が提供する拡張メソッドです。この行を実行すると、Dapper が接続を開いて DbCommand を作成し、記述したとおりにクエリを実行して、結果の行ごとに DapperDesigner オブジェクトのインスタンスを作成し、クエリ結果の値をオブジェクトのプロパティに設定します。Dapper は、プロパティ名と列名が一致しない場合や、プロパティの順序と対応する列の順序が違っている場合でも、わずかなパターンを用いて、結果の値をプロパティを対応付けることができます。ただし、何でもお見通しというわけではないので、たとえば、列の順序や名前がプロパティと同期されていない多数の文字列値が関係するマッピングを解明できるとは考えないようにします。今回はこのことを使っていくつか変わった実験を行い、Dapper が応答する方法やマッピングを推測する方法を制御するグローバル設定があるのを見つけました。

Dapper とリレーショナル クエリ

今回の DapperDesigner 型には多くのリレーションシップがあります。Products とは 1 対多、ContactInfo とは 1 対 1、Clients とは多対多のリレーションシップがあります。こうしたリレーションシップの間でクエリの実行を試してみましたが、Dapper はそのリレーションシップを処理することができました。Include メソッドやプロジェクションを使って、LINQ to EF を表現するほど簡単でないのは明白です。しかし、EF によってここ数年楽をしてきたことがたたり、筆者の TSQL スキルは限界に達していました。

以下は、データベースで直接使用することになる SQL を使って、1 対多のリレーションシップにクエリを実行する例です。

var sql = @"select * from DapperDesigners D
           JOIN Products P
           ON P.DapperDesignerId = D.Id";
var designers= conn.Query<DapperDesigner, Product,DapperDesigner>
(sql,(designer, product) => { designer.Products.Add(product);
                              return designer; });

Query メソッドでは、構築すべき両方の型を指定し、返される型 (最後の型パラメーター (DapperDesigner) によって表現) を示す必要があります。ここでは複数行のラムダ式を使用してグラフを最初に構築し、親の designer オブジェクトに関連する製品 (Products) を追加してから、Query メソッドが返す IEnumerable に各デザイナー (designer) を返しています。

筆者が最善を尽くしたこの方法の SQL の欠点は、まさに EF Include メソッドを使用したときのように、結果が平坦化されることです。デザイナーが同じでも製品ごとに 1 行取得します。Dapper には、複数の結果セットを返す MultiQuery メソッドがあります。Dapper の GridReader と組み合わせると、こうしたクエリのパフォーマンスは、明らかに EF Include メソッドよりも優れたものになります。

コーディングは難しくても実行は高速

SQL を表記し、関連するオブジェクトに値を設定することによって、これをすべて EF にバックグラウンドで処理させています。そのため、間違いなくコーディング作業は多くなります。とは言え、大量のデータを操作し、実行時のパフォーマンスを重視するのであれば、コーディングに苦労する価値は確かにあります。サンプル データベースには、約 30,000 人のデザイナーが登録されています。そのうち製品を持っているのはごくわずかです。今回、同一条件で比較する単純なベンチマーク テストをいくつか行いました。テスト結果を見る前に、このような測定をどのように行うかを理解するのに重要なポイントをいくつか説明しておきます。

既定では、EF はクエリ結果のオブジェクトを追跡するように設計されています。つまり、追跡オブジェクトが追加で作成され、そのための作業がある程度必要になり、追跡オブジェクトとの相互作用も必要になります。これとは対照的に、Dapper は結果をメモリにダンプするだけです。したがって、パフォーマンスを比べるのであれば、EF が行う変更追跡をオフにしておくことが重要です。そのため今回は、AsNoTracking メソッドを使ってすべての EF クエリを定義しています。また、パフォーマンスの比較時は、データベースの準備、複数回のクエリの反復、時間が最もかかる場合とかからない場合の除外など、標準ベンチマーク パターンの多くを適用することも必要です。今回のベンチマーク テストの構築方法の詳細については、サンプル ダウンロードを確認してください。ただし、今回は違いの考え方を示すことだけが目的なので、「軽量の」ベンチマーク テストになるように考えています。重要なベンチマークの場合は、今回の 25 回よりも多く反復し (最低でも 500 回)、テストを実行しているシステムのパフォーマンスを考慮に入れる必要があります。今回のテストは SQL Server LocalDB インスタンスを使用するノート PC で実行しているため、結果は比較目的にのみ有効です。

テストで追跡する時間は、クエリの実行と結果の構築にかかる時間です。接続や DbContext のインスタンスを作成する時間は計測しません。DbContext は再利用されるため、EF がインメモリ モデルを構築する時間は、すべてのクエリではなく、アプリケーション インスタンスごとに一度しか発生しないので、こちらも考慮しません。

今回のテスト パターンの基本構造がわかるように、Dapper と EF LINQ クエリ の "select *" テストを図 2 に示します。計測した実時間のほかに、将来の分析のため、反復ごとに時間をリスト ("times") に記録しています。

図 2 すべての DapperDesigner のクエリ時の EF と Dapper を比較するテスト

[TestMethod,TestCategory("EF"),TestCategory("EF,NoTrack")]
public void GetAllDesignersAsNoTracking() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var context = new DapperDesignerContext()) {
      _sw.Reset();
      _sw.Start();
      var designers = context.Designers.AsNoTracking().ToList();
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _trackedObjects = context.ChangeTracker.Entries().Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}
[TestMethod,TestCategory("Dapper")
public void GetAllDesigners() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var conn = Utils.CreateOpenConnection()) {
      _sw.Reset();
      _sw.Start();
      var designers = conn.Query<DapperDesigner>("select * from DapperDesigners");
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _retrievedObjects = designers.Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}

「同一条件」での比較について押えておくポイントがもう 1 つあります。Dapper は SQL そのものを受け取ります。EF のクエリは、既定では LINQ to EF によって表現するため、いくつか手を加えることで目的の SQL を構築しなければなりません。SQL を構築したら、反復作業を減らすように、パラメーターを利用する SQL であっても、アプリケーションのメモリにキャッシュされます。さらに、EF には SQL そのものを使用してクエリを実行する機能があるため、今回はこれを含め両方のアプローチを考慮に入れました。図 3 は、4 つのテスト セットの比較結果を一覧しています。ダウンロードには、もっと多くのテストを含めてあります。

図 3 25 回の反復を基に、クエリの実行とオブジェクトの設定の平均時間 (ミリ秒)、ただし最短時間と最長時間は除く

*AsNoTracking クエリ リレーションシップ LINQ to EF* EF SQL そのもの* Dapper SQL そのもの
デザイナー全員 (30,000 行) 96 98 77
製品を持つデザイナー全員 (30,000 行) 1 : * 251 107 91
クライアントを持つデザイナー全員 (30,000 行) * : * 255 106 63
契約を持つデザイナー全員 (30,000 行) 1 : 1 322 122 116

 

図 3 に示したシナリオでは、LINQ to Entities よりも Dapper を使う方が優れていることが簡単にわかります。しかし、SQL そのものを使うクエリにはわずかな違いしかないため、システム内でどちらでも使用できる特定のタスクを Dapper に切り替えることが妥当だとはかぎりません。普通、個人のニーズはさまざまで、EF クエリと Dapper の違いにこれが影響することもあります。ただし、Stack Overflow のように大量のトラフィックが行きかうシステムでは、1 回のクエリあたり数ミリ秒短縮するだけでも、かなりの効果があります。

永続化の他のニーズに対する Dapper と EF

ここまでは、返される型のプロパティを正確に一致するすべての列をテーブルから取り出すだけのシンプルなクエリを測定してきました。では、クエリを型にプロジェクションする場合はどうなるでしょう。結果のスキーマが型と一致するかぎり、オブジェクトの作成において、Dapper には違いは見受けられません。ただし、EF は、プロジェクションの結果がモデルに含まれる型に沿わない場合、多くの作業が必要になります。

DapperDesignerContext には、DapperDesigner 型向けの DbSet があります。今回のシステムには、MiniDesigner というもう 1 つの型があり、この型には DapperDesigner のプロパティのサブセットが含まれています。

public class MiniDesigner {
    public int Id { get; set; }
    public string Name { get; set; }
    public string FoundedBy { get; set; }
  }

MiniDesigner は今回の EF データ モデルには含まれていないため、DapperDesignerContext にはこの型についての知識はありません。全 30,000 行のクエリと 30,000 個の MiniDesigner オブジェクトへのプロジェクションにより、SQL そのものを使用する Dapper の方が EF よりも 25% 高速になることがわかります。繰り返しになりますが、自身のシステムを判断する際は、独自のパフォーマンス プロファイリングを行うことをお勧めします。

Dapper を使ってデータベースにデータをプッシュする場合、コマンドで指定されるパラメーターに対して使用する必要があるプロパティを特定できるメソッドを利用できます。このメソッドは、INSERT や UPDATE のコマンドそのものを使っている場合でも、データベースの関数やストアド プロシージャを使っている場合でも利用できます。今回はこのタスクについてのパフォーマンスの比較は行いませんでした。

実環境での Dapper と EF のハイブリッド使用

完璧なデータ永続化のために、数え切れないほどのシステムが Dapper を使用しています。しかし、今回このテーマを取り上げるのは、開発者たちがハイブリッド ソリューションを選択しているためです。たとえば、特定の問題領域の調整を目的にその部分に EF を導入しているシステムがあります。また、クエリにはすべて Dapper を使用し、保存にはすべて EF を使用することを選択しているチームもあります。

ハイブリッド ソリューションについて Twitter で問いかけましたが、その反応としてさまざまなフィードバックを受け取りました。

@garypochron のチームによれば、「効果の高い領域には Dapper を使い、SQL のオリジンを管理するためにリソース ファイルを使うようにしている」とのことです。人気の EF Reverse POCO Generator の製作者 Simon Hughes (@s1monhughes) が、既定では Dapper を使用し、技術的に難しい問題には EF を使用しているという、逆の方向に向かっていると知ったのは驚きでした。彼は、「可能であれば Dapper を使い、複雑な更新には EF を使う」と話しています。

また、ハイブリッド アプローチは、パフォーマンスの強化よりも、懸案事項の分離がきっかけになっていることに関するさまざまな議論も目にしました。最も多かった意見は、既定では EF の ASP.NET Identity の信頼を利用し、ソリューションの残りの永続性には Dapper を使用するというものでした。

データベースを直接操作することには、パフォーマンス以外にもメリットがあります。Rob Sullivan (@datachomp) と Mike Campbell (@angrypets) はどちらも SQL Server の専門家で、Dapper を好んで使用しています。Rob は、フル テキスト検索など、EF では利用できないデータベース機能を利用できることを指摘しています。長時間の実行では、フルテキスト検索などの機能は、実際にはパフォーマンスに関係します。

一方、変更の追跡以外にも、Dapper では実行できず、EF では実行できることがあります。この例の 1 つが、今回作成したソリューションをビルドする際に利用した機能です。それは、EF Code First Migrations を使用してデータベースをモデルの変更として移行する機能です。

ただし、Dapper は万人向けではないようです。@damiangray は、実際のデータではなく、システムのある部分から別の部分に IQueryables を返すことができる必要があるため、Dapper は選択肢にはならないと話しています。補足すると、この遅延クエリ実行というトピックは、Dapper の GitHub リポジトリ (bit.ly/22CJzJl、英語) で取り上げられています。ハイブリッド システムを設計するときは、コマンドクエリ分離 (CQS) のバリエーションを使用するのがお勧めです。つまり、トランザクションの特定の型用のモデルを分けて設計します (筆者が好きなところです)。この方法であれば、EF と Dapper の両方を操作するだけのデータ アクセス コードをビルドすることはなくなります。このような作業は、それぞれの ORM のメリットを犠牲にする結果になりかねません。本稿執筆中に、Kurt Dowswell が、「Dapper, EF and CQS」(Dapper、EF、および CQS、bit.ly/1LEjYvA、英語) というタイトルの投稿を公開しました。この記事は、すべての開発者の役に立ちます。

CoreCLR と ASP.NET Core をお考えなら、Dapper はそれらへのサポートも含むように進化しています。詳細については、Dapper の GitHub リポジトリのスレッドで確認できます (bit.ly/1T5m5Ko.、英語)。

Dapper を見て、次に考えること

では、自身についてはどうでしょう。もっと早く Dapper を確かめる時間を取らなかったことを後悔していますが、ついにそれを実現できたことには満足しています。ここでは、AsNoTracking や、データベース内でビューやプロシージャを使ってパフォーマンス問題を軽減することをいつも勧めてきました。その結果失敗したという話は聞いたことがありません。しかし、今回は、EF を使用するシステムのパフォーマンスを一気に向上させることに関心のある開発者に勧める秘策がもう 1 つあることがわかりました。ただし、それほど確信はありません。今回お勧めできるのは、Dapper を詳しく調べ、(大きな規模で) パフォーマンスの違いを測定して、パフォーマンスとコーディングの容易さのバランスを見つけることです。Stack Overflow が明らかにした使い方を考えてみてください。Stack Overflow では、質問、コメント、回答をクエリした後、いくつかのメタデータ (編集内容)、ユーザー情報、コメント、回答を含む質問のグラフを返しています。何度も繰り返し、同じ型のクエリを実行して、同じ形状の結果をマッピングしています。Dapper はこのような反復型のクエリに効果を発揮するように設計されていて、回数を重ねるごとに賢明かつ高速になります。Dapper の設計目的である膨大な量のトランザクションを持つシステムでなくても、ハイブリッド ソリューションによってニーズが満たせる点があることがわかります。


Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (@julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。

この記事のレビューに協力してくれた Stack Overflow 技術スタッフの Nick Craver と Marc Gravell に心より感謝いたします。
Nick Craver (@Nick_Craver、英語) は開発者で、サイト信頼性エンジニアであり、ときには Stack Overflow の DBA でもあります。彼はすべての層におけるパフォーマンス調整、システム全体のアーキテクチャ、データ センターのハードウェア、および Opserver などの多くのオープン ソース プロジェクトの保守を専門にしています。https://nickcraver.com/ (英語) から連絡を取ることができます。

Marc Gravell は Stack Overflow の開発者で、.NET 用の高パフォーマンス ライブラリやツール、特にデータ アクセス、シリアル化、およびネットワーク API を中心に扱っており、これらの領域のオープン ソース プロジェクトに幅広く貢献しています。