2011年5月3日火曜日

Dapper.NET

dapper-dot-net - Simple SQL object mapper for SQL Server - Google Project Hosting

これ。Massiveと同じくらい短いコードのORM。WebMatrix.Data.Databaseみたいなものですね。

オープンソースとしてたくさん存在する軽量ORMのなかでもDapperがスゴイところがパフォーマンス。どのくらい早いのかは上記サイトを確認してみると各種ORMとの速度比較が出てます。

A day in the life of a slow page at Stack Overflow

ASP.NET MVC & SQLServer & LINQ to SQLのスタックで有名な大規模サイトといえばStackoverflow.comですね。なんとそのStackoverflow.comでパフォーマンスに問題が出てきたので解決するために、このDapperを利用したというじゃないですか。

そもそもORMでN+1問題が出やすいので、LINQならjoin使ってたくさんのSQLを発行しないようにするとか、DBとのあいだのやりとりもちゃんと確認する(パフォーマンスに問題があると認識したならの話です。問題になってないなら気にしなくていいですよ)必要がありましょう。その辺はちゃんと実装したとしてもDapperが早いということです。Emitしちゃってるし。

使い方も簡単でIDbConnectionの拡張メソッドとして実装してるので、DBコネクションさえあれば簡単に導入できます。もちろんモデルクラスをPOCOで定義しておくことが最速を維持する秘訣。

AdventureWorksLT2008R2をDBに利用するためのサンプル。

public class Product
{
  public int ProductId { get; set; }
  public string Name { get; set; }
  public string ProductNumber { get; set; }
}

using (var connection = connectionFactory())
{
  var result = connection.Query<int>("select count(*) from Product").Single();
  Console.WriteLine(result + " products.");
}

// 1テーブルを1クラスにマッピング
using (var connection = connectionFactory())
{
  var result =
      connection.Query<Product>(
          "select * from Product where ListPrice between @low and @high",
          new { low = 10.0, high = 100.0 });
  Console.WriteLine("-- simple mapping:" + result.Count());
  foreach (var p in result)
  {
      Console.WriteLine(string.Format("ID:{0},Name:{1}", p.ProductId, p.Name));
  }
}

// ジェネリック指定しないで1テーブルをマッピング
using (var connection = connectionFactory())
{
  var result =
      connection.Query(
          "select * from Product where ListPrice between @low and @high",
          new { low = 10.0, high = 100.0 });
  Console.WriteLine("-- dynamic mapping:" + result.Count());
  foreach (var p in result)
  {
      Console.WriteLine(string.Format("ID:{0},Name:{1},Price:{2}", p.ProductID, p.Name, p.ListPrice));
  }
}

dapper2

ここで、実際にテーブルにはたくさん項目あるけど、モデルには省略しちゃってるんですけど、このままSQLにjoin含めた場合、ちゃんとモデルにはマッピングできませんでした。dynamicだとうまくいくんだけど、その辺の整合性はちゃんととっておかないとダメなわけですね。でもdynamicな戻り値なら早いわけではない。

dapper1

var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < 100; i++)
{
  using (var connection = connectionFactory())
  {
    var result =
        connection.Query(
            @"select p.*, c.ProductCategoryId as CategoryId, c.Name as CategoryName
          from Product as p 
            inner join ProductCategory as c on 
              p.ProductCategoryID = c.ProductCategoryID 
          where p.ListPrice between @low and @high",
            new { low = 10.0, high = 100.0 });
    foreach (var p in result){}
  }
}
stopwatch.Stop();
Console.WriteLine("dapper time:" + stopwatch.ElapsedMilliseconds + " ms");

stopwatch.Reset();
stopwatch.Start();
for (var i = 0; i < 100; i++)
{
  using (var l2s = new AwDataContext(connectionFactory()))
  {
    var result = from p in l2s.Product
                 join c in l2s.ProductCategory on p.ProductCategoryID equals c.ProductCategoryID
                 select
                     new
                         {
                             ProductId = p.ProductID,
                             p.Name,
                             Category = c.Name
                         };
    foreach (var p in result) {}
  }
}
stopwatch.Stop();
Console.WriteLine("linq2sql time:" + stopwatch.ElapsedMilliseconds + " ms");

大きなサイトへ導入されてるというのは、興味深いじゃないですか。それだけの負荷で問題が起きないように丁寧な実装がされてるわけで(どっちもだけど)。

今後、より使いやすくなることを期待しつつ見守っていきたいプロジェクトです。