1. ホーム
  2. c#

[解決済み] Dapper.Netで一対多のクエリを書くには?

2023-03-30 07:43:18

質問

1対多のリレーションを投影するためにこのコードを書きましたが、うまくいきません。

using (var connection = new SqlConnection(connectionString))
{
   connection.Open();

   IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
                        (@"Select Stores.Id as StoreId, Stores.Name, 
                                  Employees.Id as EmployeeId, Employees.FirstName,
                                  Employees.LastName, Employees.StoreId 
                           from Store Stores 
                           INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
                        (a, s) => { a.Employees = s; return a; }, 
                        splitOn: "EmployeeId");

   foreach (var store in stores)
   {
       Console.WriteLine(store.Name);
   }
}

誰か間違いに気付かないか?

EDITです。

これらは私のエンティティです。

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public IList<Store> Stores { get; set; }

    public Product()
    {
        Stores = new List<Store>();
    }
}

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public IEnumerable<Employee> Employees { get; set; }

    public Store()
    {
        Products = new List<Product>();
        Employees = new List<Employee>();
    }
}

EDITです。

クエリを変更すると

IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
        (@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
           Employees.FirstName,Employees.LastName,Employees.StoreId 
           from Store Stores INNER JOIN Employee Employees 
           ON Stores.Id = Employees.StoreId",
         (a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");

で、例外が解消されました しかし、Employeeは全くマッピングされません。との間にどんな問題があったのか、まだよく分かっていません。 IEnumerable<Employee> にどんな問題があったのか、まだよくわかりません。

どのように解決するのですか?

この投稿では 高度に正規化された SQL データベース に問い合わせ、その結果を高度にネストされたC# POCOオブジェクトの集合にマップする方法を紹介します。

成分です。

  • 8行のC#。
  • いくつかの結合を使用する、適度にシンプルなSQL。
  • 2 つの素晴らしいライブラリ。

この問題を解決することができた洞察は、2つのライブラリを分離することです。 MicroORM から mapping the result back to the POCO Entities . このように、2つの別々のライブラリを使っています。

基本的に、私たちは ダッパー を使ってデータベースに問い合わせ、次に スラッパー.オートマッパー を使って、その結果をそのままPOCOにマッピングします。

利点

  • 簡便性 . 8行以下のコードです。私はこの方が理解、デバッグ、変更がしやすいと思います。
  • より少ないコード . 数行のコードだけで スラッパー.オートマッパー は、たとえ複雑なネストしたPOCOがあったとしても(つまり、POCOには List<MyClass1> を含み、それがさらに List<MySubClass2> など)。
  • 速度 . これらのライブラリは、手作業で調整された ADO.NET クエリとほぼ同等の速度で実行できるように、非常に多くの最適化とキャッシングを備えています。
  • 懸念事項の分離 . MicroORMを別のものに変えてもマッピングは機能しますし、その逆も可能です。
  • 柔軟性 . スラッパー.オートマッパー は、任意にネストされた階層を扱うので、数レベルのネストに限定されることはない。私たちは簡単に迅速な変更を行うことができ、すべてがまだ動作します。
  • デバッギング . まず、SQLクエリが正常に動作していることを確認し、次にSQLクエリの結果が対象のPOCOエンティティに正しくマッピングされていることを確認することができます。
  • SQLでの開発のしやすさ . で平坦化されたクエリを作成するのは、とても簡単だと思います。 inner joins でフラットなクエリを作成し、フラットな結果を返すことは、クライアント側でステッチして複数の select ステートメントを作成するよりもはるかに簡単です。
  • SQL における最適化されたクエリ . 高度に正規化されたデータベースでは、フラットなクエリを作成することにより、SQL エンジンは、多くの小さな個々のクエリを構築して実行した場合には通常不可能である、全体に対する高度な最適化を適用することができます。
  • 信頼 . DapperはStackOverflowのバックエンドで、まあ、Randy Burdenはちょっとしたスーパースターです。これ以上言うことはないでしょう?
  • 開発のスピード。 非常に複雑なクエリ、多くのレベルのネストを行うことができましたが、開発時間は非常に少なかったです。
  • バグが少ない。 一度書いただけで、それがうまくいき、このテクニックは今、FTSE企業の動力源として役立っています。コードは非常に少なく、予期せぬ動作はありませんでした。

不利な点

  • 1,000,000 行を超えるスケーリングが返されます。 100,000行を返す場合はうまくいきます。しかし、>1,000,000 行を返す場合、我々と SQL サーバー間のトラフィックを減らすために、フラット化するのではなく、以下のようにします。 inner join (これは重複をもたらします) を使って平坦化するのではなく、複数の select ステートメントを使用し、クライアント側ですべてをつなぎ合わせます (このページの他の回答を参照してください)。
  • このテクニックはクエリ指向です。 . 私はこのテクニックを使ってデータベースに書き込んだことはありませんが、StackOverflow自身がDAL(Data Access Layer)としてDapperを使っているので、もう少し手を加えればDapperでも十分可能なはずです。

パフォーマンス テスト

私のテストでは スラッパー.オートマッパー はDapperが返す結果に小さなオーバーヘッドを追加しましたが、それでもEntity Frameworkの10倍速であることを意味し この組み合わせは、SQL + C# が可能な理論上の最大速度にまだかなり近いものです。 .

ほとんどの実用的なケースでは、オーバーヘッドのほとんどは、C# 側の結果のマッピングではなく、最適とは言えない SQL クエリにあるでしょう。

パフォーマンス テスト結果

反復の総数 1000

  • Dapper by itself : 1.889 ミリ秒、クエリごとに 3 lines of code to return the dynamic .
  • Dapper + Slapper.Automapper : 2.463 ミリ秒、追加の 3 lines of code for the query + mapping from dynamic to POCO Entities .

動作確認済み例

この例では Contacts のリストがあり、それぞれの Contact は一つまたはそれ以上の phone numbers .

POCOエンティティ

public class TestContact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<TestPhone> TestPhones { get; set; }
}

public class TestPhone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
}

SQLテーブル TestContact

SQL テーブル TestPhone

このテーブルには外部キー ContactID を参照しています。 TestContact テーブル(これは List<TestPhone> に相当する)。

フラットな結果を生成するSQL

この SQL クエリでは、できるだけ多くの JOIN ステートメントを必要なだけ使用し、必要なデータすべてを フラットで非正規化されたフォーム . 確かに、この方法では出力に重複が生じるかもしれませんが、これらの重複は、次のようにすれば自動的に排除されます。 スラッパー.オートマッパー を使って、このクエリの結果をそのままPOCOオブジェクト・マップに自動的にマッピングすることで、重複を排除することができます。

USE [MyDatabase];
    SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId

<イグ

C#コード

const string sql = @"SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";

string connectionString = // -- Insert SQL connection string here.

using (var conn = new SqlConnection(connectionString))
{
    conn.Open();    
    // Can set default database here with conn.ChangeDatabase(...)
    {
        // Step 1: Use Dapper to return the  flat result as a Dynamic.
        dynamic test = conn.Query<dynamic>(sql);

        // Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
        // - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
        //   let it know the primary key for each POCO.
        // - Must also use underscore notation ("_") to name parameters in the SQL query;
        //   see Slapper.Automapper docs.
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });

        var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();      

        foreach (var c in testContact)
        {                               
            foreach (var p in c.TestPhones)
            {
                Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);   
            }
        }
    }
}

出力

POCOエンティティの階層構造

Visual Studioを見ると、Slapper.AutomapperがPOCOエンティティを適切に配置したことがわかります。 List<TestContact> があり、それぞれの TestContact には List<TestPhone> .

<イグ

注意事項

DapperとSlapper.Automapperの両方は、速度のために内部で全てをキャッシュします。もし、メモリの問題が発生した場合(非常に低い確率ですが)、両方のキャッシュを時々クリアすることを確実にします。

戻ってくるカラムに名前をつけることを確実にします。 アンダースコア ( _ ) 記法を使用します。 という記法を用いて、Slapper.AutomapperにPOCOエンティティにマッピングする手がかりを与えています。

各POCOエンティティの主キーの手がかりをSlapper.Automapperに与えていることを確認してください(以下の行を参照)。 Slapper.AutoMapper.Configuration.AddIdentifiers ). また Attributes を使うこともできます。もしこのステップを飛ばしてしまうと、Slapper.Automapperがマッピングを正しく行う方法を知らないため、(理論的には)うまくいかない可能性があります。

2015-06-14更新

40以上の正規化テーブルを持つ巨大な本番データベースにこのテクニックを適用することに成功しました。16 以上の高度な SQL クエリをマッピングするために完璧に機能しました。 inner joinleft join を適切な POCO 階層(4 レベルのネスト)に変換します。クエリは非常に高速で、ADO.NET でハンドコーディングするのとほぼ同じ速度です(通常、クエリに 52 ミリ秒、フラットな結果から POCO 階層へのマッピングに 50 ミリ秒かかりました)。これは本当に革命的なことではありませんが、速度と使いやすさの点でEntity Frameworkを確実に上回り、特に私たちが行っていることがクエリを実行しているだけであれば、それは間違いありません。

更新日 2016-02-19

コードは9ヶ月間、本番で完璧に動作しています。最新バージョンの Slapper.Automapper には、SQL クエリで返される NULL に関連する問題を修正するために適用したすべての変更が含まれています。

更新日 2017-02-20

コードは21ヶ月間、本番環境で完璧に動作しており、FTSE250企業の数百人のユーザーからの継続的なクエリを処理しています。

Slapper.Automapper は、.csv ファイルを POCO のリストに直接マッピングするのにも最適です。.csv ファイルを IDictionary のリストに読み込んで、それをターゲットとなる POCO のリストに直接マッピングします。唯一のトリックは、プロパティに int Id {get; set} を追加し、それが各行で一意であることを確認することです(さもなければ、オートマッパーは行を区別することができません)。

2019-01-29 更新

より多くのコードコメントを追加するマイナーアップデート。

参照してください。 https://github.com/SlapperAutoMapper/Slapper.AutoMapper