9
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【C#】LtQueryがDapper・EFCoreを超したかも知れない

Last updated at Posted at 2023-11-01

今回は LtQuery(リトル クエリ) を紹介するエントリです。
実はただの自己布教活動。他人事の様に語っていますが自己布教活動です。

LtQuery

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

GitHubから抜粋

ORM Mean Error StdDev Gen0 Gen1 Allocated
ADO.NET 3.883 ms 0.0633 ms 0.0592 ms 296.8750 203.1250 1.44 MB
LtQuery 3.906 ms 0.0380 ms 0.0337 ms 296.8750 195.3125 1.44 MB
Dapper 4.416 ms 0.0255 ms 0.0226 ms 359.3750 187.5000 1.62 MB
EFCore 6.816 ms 0.0720 ms 0.0673 ms 554.6875 367.1875 2.6 MB

インストール

SQL Serverの場合

dotnet add package LtQuery.SqlServer

MySQL(MariaDB)の場合

dotnet add package LtQuery.MySql

SQLiteの場合

dotnet add package LtQuery.Sqlite

使用例

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時に消される仕組みです。

LtQueryではEF Coreのようにエンティティ構成を事前に定義する必要があります。

class ModelConfiguration : IModelConfiguration
{
    public void Configure(IModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(b =>
        {
            b.HasProperty(_ => _.Id, true);
            b.HasProperty(_ => _.Name);
            b.HasProperty(_ => _.Email);
        });
        modelBuilder.Entity<Blog>(b =>
        {
            b.HasProperty(_ => _.Id, true);
            b.HasProperty(_ => _.Title);
            b.HasReference(_ => _.UserId, _ => _.User, _ => _.Posts);
            b.HasProperty(_ => _.DateTime);
            b.HasProperty(_ => _.Content);
        });
        modelBuilder.Entity<Post>(b =>
        {
            b.HasProperty(_ => _.Id, true);
            b.HasReference(_ => _.BlogId, _ => _.Blog, _ => _.Posts);
            b.HasProperty(_ => _.DateTime);
            b.HasProperty(_ => _.Content);
        });
    }
}

この辺はEF Coreそっくりですね。現在のバージョンではメソッドの順番がコンストラクタの引数の順番と一致していなくてはいけません

エンティティの実装は以下の感じです。

public class Blog
{
    public int Id { get; private set; }
    public string Title { get; private set; }
    public int UserId { get; private set; }
    public DateTime DateTime { get; private set; }
    public string Content { get; private set; }

    // ナビゲーション
    public User User { get; set; } = default!;
    public List<Post> Posts { get; } = new();

    public Blog(int id, string title, int categoryId, int userId, DateTime dateTime, string content)
    {
        Id = id;
        Title = title;
        CategoryId = categoryId;
        UserId = userId;
        DateTime = dateTime;
        Content = content;
    }
}

コンストラクタでパラメータをインジェクションする方式です。この方がDDDらしいコードで個人的には好み。
デフォルトコンストラクタでの生成は対応していないみたいです。

次にセットアップの例です。

var collection = new ServiceCollection();
collection.AddLtQuerySqlServer(new ModelConfiguration(), _ => new SqlConnection(/*ConnectionString*/);
var provider = collection.BuildServiceProvider();

using(var scope = provider.CreateScope())
{
	// get ILtConnection
	var connection = scope.ServiceProvider.GetRequiredService<ILtConnection>();
    :
    :
}

DIコンテナが使用されており、現バージョンではMicrosoft.Extensions.DependencyInjectionのみの対応しています。ILtConnection は コネクションプールから接続を生成しているため EF Core と同様再生成が速いです。

何故速いか

まず、クエリをオブジェクト表現(Query<>)することでキャッシュが容易である点と、このクエリオブジェクトが超軽量でこれが全ての動作の起点となっている点です。
例えばEF Coreの場合はLINQ to SQLが中枢に存在します。ということはExpressionに依存しきっていることになり、Expressionはそこそこ重いためどうしても全体に影響が出てしまう訳です。Dapperの場合はSQL(string型)が起点となっており、軽量ではあるのですが実行するまでクエリの構造を把握できないデメリットがあります。つまりはクエリをどう表現するかがとても大事な訳です。

そして肝心のReaderから得たデータをエンティティに変換するメソッドが完全IL実装されています。いわゆる黒魔術です。これが理論上ADO.NETべた書きと同等な処理速度を実現している訳です。ただメソッド生成にはそれなりのコストが掛かります。こういう重い処理をキャッシュの向こう側に追いやることで軽量に保っていられるのです。重い処理といってもEF Coreよりは速いんですけどね。

デメリット

  • ふと出現したまだ不安定なライブラリ
  • 現在SQL Serverにしか対応していない 最新版ではMySQL(MariaDB)にも対応しています
  • INSERT/UPDATE/DELETが未対応 最新版で対応しています
  • ドキュメントが少ない

今後の動向

DDDに最適なORMを目指しているという事ですので機能的にはValue Object対応などが優先されるかと思われる。

関連記事

後書き

という事で自己布教でした。GitHubの方では英語オンリーなので日本語の解説を何処かに書き殴りたくて結果これです。

Thanks

9
13
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?