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

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

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

※[GitHub](https://github.com/SaikiMikami/LtQuery)から抜粋
| 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の場合
```powershell
dotnet add package LtQuery.SqlServer
```
MySQL(MariaDB)の場合
```powershell
dotnet add package LtQuery.MySql
```
SQLiteの場合
```powershell
dotnet add package LtQuery.Sqlite
```

## 使用例


```csharp
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のようにエンティティ構成を事前に定義する必要があります。

```csharp
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そっくりですね。現在のバージョンでは**メソッドの順番がコンストラクタの引数の順番と一致していなくてはいけません**。

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

```csharp
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らしいコードで個人的には好み。
デフォルトコンストラクタでの生成は対応していないみたいです。

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

```csharp
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対応などが優先されるかと思われる。

## 関連記事

- [【C#】高速ORM「LtQuery」の書き込み処理](https://qiita.com/maikami03/items/60cfa6990e2b19bf3eaf)

## 後書き

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

## Thanks
- [メタプログラミングの師 Yoshifumi Kawai](https://neue.cc/2017/12/04_560.html)