【C#】LtQueryなる高速なORMを作った

概要

LtQuery(リトルクエリ)はEF CoreのようにLINQやInclude()でSQLを構築する機能が備わっていてかつ、パフォーマンスがDapperを凌いでいるといういいとこ取りの高速O/Rマッパー(ORM)です。
クエリキャッシュが強力で一度実行したクエリはADO.NET直書きに迫る速度が出ます。キャッシュ自体も遅くはなくEF Core よりも断然速いです。更に使用メモリ量もかなり抑えられています。

ORMMeanErrorStdDevGen0Gen1Allocated
ADO.NET3.883 ms0.0633 ms0.0592 ms296.8750203.12501.44 MB
LtQuery3.906 ms0.0380 ms0.0337 ms296.8750195.31251.44 MB
Dapper4.416 ms0.0255 ms0.0226 ms359.3750187.50001.62 MB
EF Core6.816 ms0.0720 ms0.0673 ms554.6875367.18752.6 MB

使用例

詳細はGitHub参照。

class BlogService
{
    readonly ILtConnection _connection;
    public BlogService(ILtConnection connection)
    {
        _connection = connection;
    }

    // クエリ生成
    static readonly Query<Blog> _query = Lt.Query<Blog>()
        .Include(_ => _.Posts).Where(_ => _.UserId == Lt.Arg<int>("UserId"))
        .OrderByDescending(_ => _.Date).Take(20).ToImmutable();

    public IEnumerable<Blog> GetNewBlogs(int userId)
    {
        // クエリ実行
        return _connection.Select(_query, new { UserId = userId });
    }
}

パラメータにしたい箇所を Lt.Arg<int>("UserId") の様に記述し、クエリ実行時に匿名型で渡す仕組みです。クエリのキャッシュ方法は実に簡単で単にフィールドなどに保持しておくだけ。内部的に弱参照を使っているため保持していなければGC時にキャッシュごと消される仕組みです。LINQに依存しているように見えますがそれは生成の時だけで Query<> はほぼプリミティブ型の塊です。

背景

  • 「Entity Framework ってCoreになっても遅いですよね?」
  • 「DapperはSQLすぎる」

ってな訳でO/Rマッパー(ORM)を自作することにしました。

Entity Framework Core の問題点

LINQ to SQLに依存しきっている

EF CoreLINQ to SQL が中枢に存在します。ということは Expression に依存しきっていることになり、 Expression はそこそこ重いためどうしても全体に影響が出てしまいます。システム的に限界が見えている訳です。

明示的クエリキャッシュがくどい

static readonly Func<TestContext, int, Blog> _selectSingle = 
    EF.CompileQuery((TestContext context, int id) => 
      context.Set<Blog>().Single(_ => _.Id == id));

EF Coreは自動的にクエリキャッシュを行うため気にしなくてもいいです。ですが、あえて明示的に行おうとすると↑のようなコードになります。

Dapper の問題点

SQL依存

DapperはSQLファーストなライブラリです。超シンプルで素晴らしいのですが、やはり機能面が物足りない。それを補うライブラリ( SqlKataDapper.FastCrud など)がありますが、SQLが中枢部にいると拡張しづらい問題があります。

これらの問題を解決するORMのシステム

まず、中枢から ExpressionSQL を排除します。代わりに軽量なクエリオブジェクトを中枢におき、すべての動作の核とします。そこからSQLを構築したり、読み取り処理のフロー決定に使ったりします。このクエリオブジェクトをキーにしてクエリキャッシュを行います

そして、更にパフォーマンスを向上するため読み取り処理は完全IL実装にします。この辺はSQLファーストだったら実装しづらい機能です。Dapper に近いパフォーマンスを目標にします。Dapper を超しました。

今後

ベンチ見てやる気が出たので本格的なフルORMにする予定です。特にValue ObjectとかDDD絡みの機能を追加してDDDに最適なORMを目指したいと思います。

コメント

タイトルとURLをコピーしました