web-dev-qa-db-ja.com

Dapper.Netで1対多のクエリを作成するにはどうすればよいですか?

私はこのコードを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);
   }
}

誰でも間違いを見つけることができますか?

編集:

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

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>();
        }
    }

編集:

クエリを次のように変更します。

            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");

そして、私は例外を取り除きます!ただし、従業員はまったくマッピングされません。最初のクエリでIEnumerable<Employee>にどのような問題があったのかまだわかりません。

70
TCM

この投稿では、 高度に正規化されたSQLデータベース を照会し、結果を高度にネストされたC#POCOオブジェクトのセットにマッピングする方法を示します。

材料:

  • C#の8行。
  • いくつかの結合を使用する合理的に単純なSQL。
  • 2つの素晴らしいライブラリ。

この問題を解決することができた洞察は、MicroORMmapping the result back to the POCO Entitiesから分離することです。したがって、2つの個別のライブラリを使用します。

基本的に、 Dapper を使用してデータベースを照会し、次に Slapper.Automapper を使用して結果を直接POCOにマッピングします。

長所

  • シンプルさ。 8行未満のコード。これは、理解、デバッグ、変更がはるかに簡単だと思います。
  • 少ないコード。数行のコードですべて Slapper.Automapper 入れ子になった複雑なPOCO(つまり、POCOにList<MyClass1>が含まれ、さらにList<MySubClass2>など)。
  • 速度。これらのライブラリは両方とも、手作業で調整されたADO.NETクエリとほぼ同じ速度で実行できるように、並外れた量の最適化とキャッシングを備えています。
  • 懸念の分離。 MicroORMを別のものに変更することもできますが、マッピングは引き続き機能し、その逆も同様です。
  • 柔軟性Slapper.Automapper は、任意にネストされた階層を処理します。ネストのレベルは2、3に制限されていません。迅速な変更を簡単に行うことができ、すべてが引き続き機能します。
  • デバッグ。最初に、SQLクエリが適切に機能していることを確認してから、SQLクエリの結果がターゲットPOCOエンティティに正しくマッピングされていることを確認できます。
  • SQLの開発の容易さinner joinsを使用してフラット化されたクエリを作成し、フラットな結果を返すことは、クライアント側でステッチして複数のselectステートメントを作成するよりもはるかに簡単です。
  • SQLの最適化されたクエリ。高度に正規化されたデータベースでは、フラットクエリを作成することにより、SQLエンジンが高度な最適化を全体に適用できるようになります。
  • Trust。 DapperはStackOverflowのバックエンドであり、まあ、Randy Burdenはちょっとしたスーパースターです。もう言う必要がありますか?
  • 開発の速度。ネストのレベルが多く、非常に複雑なクエリをいくつか実行でき、開発時間は非常に短かった。
  • バグを少なくしました。一度書いただけでうまくいきましたが、この手法は現在FTSE企業を支援しています。コードが非常に少ないため、予期しない動作はありませんでした。

欠点

  • 1,000,000行を超えるスケーリングが返されます。100,000行未満を返す場合に適切に機能します。ただし、1,000,000を超える行を戻す場合は、SQLサーバーとの間のトラフィックを減らすために、inner join(重複を戻す)を使用してフラット化しないで、代わりに複数のselectステートメントを作成し、クライアント側ですべてをつなぎ合わせます(このページの他の回答を参照)。
  • この手法はクエリ指向です。データベースへの書き込みにこの手法を使用したことはありませんが、StackOverflow自体がデータアクセスレイヤー(DAL)としてDapperを使用しているため、Dapperはこれ以上の追加作業を行うことができると確信しています。

性能試験

私のテストでは、 Slapper.Automapper はDapperが返す結果に小さなオーバーヘッドを追加しました。つまり、Entity Frameworkよりも10倍高速であり、それでも、SQL + C#が可能な理論上の最大速度にかなり近い

ほとんどの実際の場合、オーバーヘッドの大部分は、C#側での結果のマッピングではなく、最適ではないSQLクエリにあります。

性能試験結果

反復の総数:1000

  • Dapper by itself1.889クエリごとのミリ秒、3 lines of code to return the dynamicを使用。
  • Dapper + Slapper.Automapper2.463クエリごとのミリ秒。追加の3 lines of code for the query + mapping from dynamic to POCO Entitiesを使用します。

実施例

この例では、Contactsのリストがあり、各Contactには1つ以上の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

enter image description here

SQLテーブルTestPhone

このテーブルには、ContactIDテーブルを参照する外部キーTestContactがあることに注意してください(これは上記のPOCOのList<TestPhone>に対応します)。

enter image description here

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

SQLクエリでは、必要なすべてのデータを取得するために必要な数のJOINステートメントを flat、denormalized form で使用します。はい、これは出力に重複を生成する可能性がありますが、これらの重複は Slapper.Automapper を使用してこのクエリの結果を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

enter image description here

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);   
            }
        }
    }
}

出力

enter image description here

POCOエンティティ階層

Visual Studioを見ると、Slapper.AutomapperがPOCOエンティティに適切に入力されていることがわかります。つまり、List<TestContact>があり、各TestContactにはList<TestPhone>があります。

enter image description here

ノート

DapperとSlapper.Automapperは両方とも、速度のためにすべてを内部的にキャッシュします。メモリの問題に遭遇した場合(非常にまれ)、両方のキャッシュを時々クリアするようにしてください。

アンダースコア(_)表記 を使用して、返される列に名前を付けて、Slapper.Automapperが結果をPOCOエンティティにマップする方法の手がかりを与えるようにします。

Slapper.Automapperが各POCOエンティティのプライマリキーの手がかりを与えるようにしてください(Slapper.AutoMapper.Configuration.AddIdentifiers行を参照)。このために、POCOでAttributesを使用することもできます。この手順をスキップすると、Slapper.Automapperはマッピングを適切に行う方法を知らないため、(理論的には)間違った方向に進む可能性があります。

更新2015-06-14

40以上の正規化されたテーブルを持つ巨大な実稼働データベースにこの手法をうまく適用しました。 16以上のinner joinおよびleft joinを含む高度なSQLクエリを適切なPOCO階層(4レベルのネスト)にマッピングするのに完全に機能しました。クエリは、ADO.NETで手作業でコーディングするのとほぼ同じくらい高速です(通常、クエリでは52ミリ秒、フラットな結果からPOCO階層へのマッピングでは50ミリ秒でした)。これは本当に画期的なものではありませんが、特にクエリを実行するだけの場合は、速度と使いやすさでEntity Frameworkに勝ります。

更新2016-02-19

コードは9か月間実稼働で問題なく実行されています。 Slapper.Automapperの最新バージョンには、SQLクエリで返されるnullに関連する問題を修正するために適用したすべての変更が含まれています。

更新2017-02-20

コードは21か月間実稼働で問題なく実行されており、FTSE 250企業の何百人ものユーザーからの継続的なクエリを処理しています。

Slapper.Automapperは、.csvファイルをPOCOのリストに直接マッピングするのにも最適です。 .csvファイルをIDictionaryのリストに読み込んでから、POCOのターゲットリストに直接マップします。唯一のトリックは、適切なint Id {get; set}を追加し、すべての行で一意であることを確認する必要があることです(そうしないと、オートマッパーは行を区別できなくなります)。

更新2019-01-29

コードコメントを追加するためのマイナーアップデート。

参照: https://github.com/SlapperAutoMapper/Slapper.AutoMapper

143
Contango

私はそれをできるだけシンプルにしたかった、私の解決策:

public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
    var sql = @"
    select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, 
        d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, 
        d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
    from 
        t_data d
    where d.cd_data = @DataId order by id_data asc;

    select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
    from 
        t_data d
        inner join T_data_image di on d.id_data = di.cd_data
        inner join T_image i on di.cd_image = i.id_image 
    where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";

    var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
    var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
    var images = mapper.Read<ForumMessageImage>().ToList();

    foreach(var imageGroup in images.GroupBy(g => g.DataId))
    {
        messages[imageGroup.Key].Images = imageGroup.ToList();
    }

    return messages.Values.ToList();
}

私はまだデータベースへの呼び出しを1回行い、1つではなく2つのクエリを実行していますが、2番目のクエリは最適でないLEFT結合ではなくINNER結合を使用しています。

15
Davy

この答え によると、Dapper.Netに組み込まれているマッピングサポートは1対多ではありません。クエリは、データベース行ごとに常に1つのオブジェクトを返します。ただし、代替ソリューションが含まれています。

7
Damir Arh

GetHashCodeの代わりにFuncを使用して親キーを選択するAndrewの答えのわずかな修正。

public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
    this IDbConnection connection,
    string sql,
    Func<TParent, TParentKey> parentKeySelector,
    Func<TParent, IList<TChild>> childSelector,
    dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
    Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();

    connection.Query<TParent, TChild, TParent>(
        sql,
        (parent, child) =>
            {
                if (!cache.ContainsKey(parentKeySelector(parent)))
                {
                    cache.Add(parentKeySelector(parent), parent);
                }

                TParent cachedParent = cache[parentKeySelector(parent)];
                IList<TChild> children = childSelector(cachedParent);
                children.Add(child);
                return cachedParent;
            },
        param as object, transaction, buffered, splitOn, commandTimeout, commandType);

    return cache.Values;
}

使用例

conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
7
Clay

これは大まかな回避策です

    public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
    {
        var cache = new Dictionary<int, TOne>();
        cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
                                            {
                                                if (!cache.ContainsKey(one.GetHashCode()))
                                                    cache.Add(one.GetHashCode(), one);

                                                var localOne = cache[one.GetHashCode()];
                                                var list = property(localOne);
                                                list.Add(many);
                                                return localOne;
                                            }, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
        return cache.Values;
    }

決して最も効率的な方法というわけではありませんが、起動して実行できます。機会があれば、これを最適化してみます。

次のように使用します。

conn.Query<Product, Store>("sql here", prod => prod.Stores);

オブジェクトはGetHashCodeを実装する必要があることに留意してください。おそらく次のようになります。

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }
2
Andrew Bullock

別の方法を次に示します。

注文(1)-OrderDetail(多く)

using (var connection = new SqlCeConnection(connectionString))
{           
    var orderDictionary = new Dictionary<int, Order>();

    var list = connection.Query<Order, OrderDetail, Order>(
        sql,
        (order, orderDetail) =>
        {
            Order orderEntry;

            if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
            {
                orderEntry = order;
                orderEntry.OrderDetails = new List<OrderDetail>();
                orderDictionary.Add(orderEntry.OrderID, orderEntry);
            }

            orderEntry.OrderDetails.Add(orderDetail);
            return orderEntry;
        },
        splitOn: "OrderDetailID")
    .Distinct()
    .ToList();
}

ソースhttp://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping- 1対多

1
Exocomp