今回は 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の方では英語オンリーなので日本語の解説を何処かに書き殴りたくて結果これです。