私がノルム対ダッパーを使うのを好む3つの理由


Norm data-access library for .NET 楽しいペットプロジェクトとしてスタートした(これらのHomebrew MicroOmのプロジェクトの多くがそうであるように)、そして、今、私はそれが最新のバージョン3.1でその満期に達したと思っています.
すべてのこれらの小さなMicroOmの共有は、同じ基本的な機能を、例えば、すべてのSQLクエリの結果を選択したデータ構造にマップされます.
例えば、以下のようなクラスがあるならば:
public class OrderDetail
{
    public int OrderDetailID { get; set; }
    public int OrderID { get; set; }
    public int ProductID { get; set; }
    public int Quantity { get; set; }
}
そして、クエリから結果をマップしたかったSELECT TOP 10 * FROM OrderDetails , Dapperの例は、一般的に次のようになります.
using var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools());

var sql = "SELECT TOP 10 * FROM OrderDetails";
var orderDetails = connection.Query<OrderDetail>(sql);

// output the results
Console.WriteLine(orderDetails.Count());                
FiddleHelper.WriteTable(orderDetails);
Try it yourself
同様に、ノルムの例は次のようになります.
using var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools());

var sql = "SELECT TOP 10 * FROM OrderDetails";
var orderDetails = connection.Read<OrderDetail>(sql);

// output the results
Console.WriteLine(orderDetails.Count());                
FiddleHelper.WriteTable(orderDetails);
Try it yourself
それは実に非常に似ている.
しかし、私にとっては、少なくとも3つの良い理由があります.なぜ私のプロジェクトでdapperの上でNORMを使用したいか.
はい、どうぞ.

1すべてのモデルクラスを持っていない.
私は、単に新しいクラス(またはより新しいバージョンの記録)モデルを作成するのに使用されると思います.
それは常に行われた方法です.
もちろん、すべてのクエリがモデルクラスを持っている必要はありません.典型的な例としては、例えばcountクエリーです.
SELECT COUNT(*) 
FROM OrderDetails
このような場合、DapperはQuery . 例えば、
var sql = "SELECT COUNT(*) FROM OrderDetails";
var count = connection.QuerySingle<int>(sql);
NORMはまだ同じRead メソッドですが、LINQ拡張モジュールを使用しますSingle 最初の結果を得るには、次の手順に従います.
var sql = "SELECT COUNT(*) FROM OrderDetails";
var count = connection.Read<int>(sql).Single();
さて、1つではなく2つの値を返すクエリから結果を得たい場合はどうでしょうか.
その場合、モデルクラスを作成することは、まだ私の意見においては過大です.
例えば、ミニマムと言いましょうOrderID そのIDの総カウント値.
クエリは次のようになります.
SELECT TOP 1 min(OrderID) as OrderID, COUNT(*) 
FROM OrderDetails 
GROUP BY OrderID
これはDAPPERで可能ですが、マルチマッピング機能を使用しなければなりません.
var sql = @"
SELECT TOP 1 min(OrderID) as OrderID, COUNT(*) 
FROM OrderDetails 
GROUP BY OrderID
";
var (orderId, count) = connection.Query<int, int, (int, int)>(sql,
    (orderId, count) => (orderId, count), 
    splitOn: "OrderID").Single();
ノルムは、まだ同じを使用しますRead メソッド:
var sql = @"
SELECT TOP 1 min(OrderID), COUNT(*) 
FROM OrderDetails 
GROUP BY OrderID
";
var (orderId, count) = connection.Read<int, int>(sql).Single();
この場合、min(OrderID) NormはAを必要としないので、名前別名を持っていませんsplitOn 値をnameで区別するパラメータです.
これらの2つの値は、表示される位置によってマップされ、名前はこの場合完全に無関係です.
これはきちんとしています.なぜなら、私たちが単に2、3の値を必要とするなら、私たちは本当にクラスを作成する必要はありません.
例えば、3つの値、製品名、製品単位、および製品価格:
var sql = "SELECT TOP 10 ProductName, Unit, Price FROM Products";
foreach(var (name, unit, price) in connection.Read<string, string, decimal>(sql))
{
    Console.WriteLine($"{name} has price {price} per {unit}");
}
Try it yourself
一般的に、上記の例では、これらの3つの値を必要とするだけで、おそらくいくつかの出力やいくつかのアルゴリズムでは、モデルクラスを作成したくないかもしれません.
ここでは、辞書キーが製品IDであり、辞書値が製品名である製品のデータベースから辞書を作りたいと言いましょう.
標準式は次のようになります.
var sql = "SELECT TOP 10 ProductID, ProductName FROM Products";
var dict = connection.Read<int, string>(sql).ToDictionary(p => p.Item1, t => p.Item2);
Try it yourself
これは私たちがフィールド名を失ったので、少しsuboptimalですItem1 and Item2 名称
複雑なLINQ式では、これは重大な問題でありえます.
これはnormが名前付きタプルをサポートする理由です.
したがって、上記の例は次のようになります.
var sql = "SELECT TOP 10 ProductID, ProductName FROM Products";
var dict = connection.Read<(int Id, string Name)>(sql).ToDictionary(p => p.Id, t => p.Name);
Try it yourself
これは今ではずっと良いです.なぜなら、私たちにはまだ強いタイプがあります.また、完全なIDEとIntelliSenseエディタのサポートとともに、適切な名前も持っています.
そして、我々はまだモデルクラスを作成する必要はありませんでした.
しかし、もちろん、あなたが本当にしたい場合.
そして、おそらくフィールドはもはや位置によってマップされないが、むしろ名前によってマップされるので、それはおそらく賢い決定です.

2 .デフォルトでの列挙発電
Dapperに慣れている人は、Dapperのクエリメソッドが基本的に二つの異なるバージョンを持っていることを覚えておいてください.
  • buffered (デフォルト): dapperは結果からリスト全体を構築し、クエリメソッドを返します.
  • Unbuffered : Dapperは実際の反復の結果を生成する列挙型ジェネレータを作成し、クエリメソッドは実際のリスト構造の代わりにそのジェネレータを返します.
  • バッファされたバージョンは、いくつかの理由のための準最適解です.例えば、
  • 同じデータ上の複数の反復の可能性.一度実際のリストと2番目の時間をビルドしてコード内のデータを使用して(UIをレンダリングし、サービスの結果などを生成する).
  • あなたが最初の場所でリストを構築する必要はありません.何らかの理由で配列や辞書を使うことを好むかもしれません.
  • 結果から最初の行だけを取得したい場合は、最初の行の後に結果リストをビルドする反復を停止する方法はありません.このデザイン決定の結果として、Dapperは最初の行の後にイテレータ停止を作る別々の方法を持たなければなりません.それが方法QuerySingle and QueryFirst 用です.
  • 今、私はこの決定の背後にある本当の理由は、非バッファのクエリの前に非同期バージョンを実装する方法がなかったからです.NET標準2.1 .および.NETコア3
    あなたが戻らなければならないからIAsyncEnumerable の代わりにTask<IEnumerable> それを達成するために、そのインターフェースはそれらのバージョンの前に存在しませんでした.
    したがって、同期バージョンのクエリをバッファリングされていないメソッドとasyncだけでバッファリングされたバージョンでのみ良い妥協したと信じています.
    一方、規範は厳密です.NET標準2.1 .図書館.つまり、以下のようにサポートされます.NETの実装
  • .ネット5
  • .NETコア3.0と3.1
  • モノ6.4
  • ザマリンIOSの12.16
  • ザマリンアンドロイド10.0
  • そして、そのように、それはバッファなしのバージョンを実装するだけです.それは、ノルムRead メソッドは、実際の反復の結果を生成する列挙型ジェネレータを作成して返します.
    これを行う決定は本当に簡単だった.サポートされていないバージョン(例えば. NET Frameworkなど)で同じ機能を使用する場合は、常にdapperを使用できます.それはあなたが必要とするほとんどすべてをカバーします.
    例えば、最初の行だけをNORMでyieldするには、LINQ拡張子を使用できますSingle :
    var sql = "SELECT COUNT(*) FROM OrderDetails";
    var count = connection.Read<int>(sql).Single();
    
    Single 最初の読み込み後に反復処理を停止します.
    dapperでコードを実行すると、結果セット全体が反復され、最初の結果が返されます.
    var sql = "SELECT OrderID FROM OrderDetails";
    var count = connection.Query<int>(sql).Single();
    
    そのパフォーマンスの落とし穴を回避するには、バッファー付きのバージョンに戻る必要があります.
    var sql = "SELECT OrderID FROM OrderDetails";
    var count = connection.Query<int>(sql, buffered: false).Single();
    
    しかし、デフォルトでバッファなしのバージョンでは、他の利点があります.たとえば、辞書や配列などの他のデータ構造を好み、リスト部分を完全にスキップします.
    var sql = "SELECT OrderID FROM OrderDetails";
    var orderIds = connection.Read<int>(sql).ToArray();
    
    またはdapperがあります:
    var sql = "SELECT OrderID FROM OrderDetails";
    var orderIds = connection.Query<int>(sql, buffered: false).ToArray();
    
    Normがすべての読み込み操作のために1つの拡張/メソッドだけを持つことができる理由Read .
    非同期バージョンでは、標準のLINQ式はIEnumerable インタフェースではなくIAsyncEnumerable .
    解決策は System.Linq.Async プロジェクトに.
    このライブラリは、言語統合クエリIAsyncEnumerable<T> シーケンス.
    これは、標準のLINQライブラリと同じ拡張機能を実装します.
    これは、作成され、";ネット財団と貢献者";でサポートされており、それは3以上、これまでの百万ダウンロードがあります.
    また、同じ名前空間を使用します.
    したがって非同期Single 上記の例は次のようになります.
    var sql = "SELECT COUNT(*) FROM OrderDetails";
    var count = await connection.Read<int>(sql).SingleAsync();
    
    しかし、あなたがそうすることができる1つの強力なasync機能もありますIAsyncEnumerable<T> 結果.
    これは非同期ストリーミングです.ノルムを使用すると、非同期で反復することができますforeach 結果について例えば、
    // Asynchronously stream values directly from database
    var sql = "SELECT TOP 10 OrderId, ProductId, Quantity FROM OrderDetails";
    await foreach(var (orderId, productId, quantity) in connection.ReadAsync<int, int, int>(sql))
    {
        Console.WriteLine($"order={orderId}, product={productId} with quantity {quantity}");
    }
    
    Try it yourself
    これは、すぐに彼らが表示されるように接続から結果を得ることができますし、より良いパフォーマンスとスケーラビリティを与える非同期のストリーミング.

    PostgreSQL
    そして最後に、私を幸せにした小さなもの.
    私はかなり重いPostgreSQLユーザです、そして、それは選択の私のデータベースです.
    したがって、PostgreSQL開発者のための重要な機能は、追加の設定なしでボックスから利用できます.
  • スネークケースマッピング.クラスまたはレコードへのマッピング時には、標準であなたのクエリにあるすべてのスネークケースの名前をマップします.
  • 配列型サポート.PostgreSQLデータベースクエリが配列を返す場合、問題はありません.int[] , string[] , DateTime[] , など).単純な型、タプル、またはクラスとレコードのマッピング時と同じように動作します.