84

文書化された投稿/ユーザーの例によく似ているように見えるので、私はこれを少しいじっていますが、少し異なり、私にとってはうまくいきません.

次の単純化されたセットアップを想定しています (連絡先には複数の電話番号があります)。

public class Contact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public IEnumerable<Phone> Phones { get; set; }
}

public class Phone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
    public string Type { get; set; }
    public bool IsActive { get; set; }
}

複数の Phone オブジェクトを持つ Contact を返すものに行き着きたいと思っています。そうすれば、2 つの連絡先があり、それぞれに 2 つの電話がある場合、SQL はそれらの結合を合計 4 行の結果セットとして返します。次に、Dapper は、それぞれ 2 つの電話を持つ 2 つの連絡先オブジェクトをポップアウトします。

ストアド プロシージャ内の SQL は次のとおりです。

SELECT *
FROM Contacts
    LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1

私はこれを試しましたが、4つのタプルになりました(これは問題ありませんが、私が望んでいたものではありません...結果を再正規化する必要があることを意味します):

var x = cn.Query<Contact, Phone, Tuple<Contact, Phone>>("sproc_Contacts_SelectByClient",
                              (co, ph) => Tuple.Create(co, ph), 
                                          splitOn: "PhoneId", param: p, 
                                          commandType: CommandType.StoredProcedure);

別の方法 (下記) を試すと、「'System.Int32' 型のオブジェクトを 'System.Collections.Generic.IEnumerable`1[Phone]' 型にキャストできません」という例外が発生します。

var x = cn.Query<Contact, IEnumerable<Phone>, Contact>("sproc_Contacts_SelectByClient",
                               (co, ph) => { co.Phones = ph; return co; }, 
                                             splitOn: "PhoneId", param: p,
                                             commandType: CommandType.StoredProcedure);

私は何か間違ったことをしているだけですか?投稿/所有者の例と同じように見えますが、子から親ではなく親から子に移動する点が異なります。

前もって感謝します

4

9 に答える 9

70

あなたは何も悪いことをしていません、それはAPIが設計された方法ではありません。すべてのQueryAPIは、データベース行ごとに常にオブジェクトを返します。

したがって、これは多くの->一方向ではうまく機能しますが、1つ->多くのマルチマップではうまく機能しません。

ここには2つの問題があります。

  1. クエリで機能する組み込みのマッパーを導入すると、重複するデータを「破棄」することが期待されます。(Contacts。*はクエリで重複しています)

  2. 1つ->多数のペアで機能するように設計する場合、ある種のIDマップが必要になります。これにより複雑さが増します。


たとえば、限られた数のレコードをプルする必要がある場合に効率的なこのクエリを考えてみましょう。これを100万までプッシュすると、トリッキーになり、ストリーミングが必要になり、すべてをメモリにロードできなくなります。

var sql = "set nocount on
DECLARE @t TABLE(ContactID int,  ContactName nvarchar(100))
INSERT @t
SELECT *
FROM Contacts
WHERE clientid=1
set nocount off 
SELECT * FROM @t 
SELECT * FROM Phone where ContactId in (select t.ContactId from @t t)"

あなたができることはGridReader、再マッピングを可能にするためにを拡張することです:

var mapped = cnn.QueryMultiple(sql)
   .Map<Contact,Phone, int>
    (
       contact => contact.ContactID, 
       phone => phone.ContactID,
       (contact, phones) => { contact.Phones = phones };  
    );

GridReaderを拡張し、マッパーを使用するとします。

public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
    (
    this GridReader reader,
    Func<TFirst, TKey> firstKey, 
    Func<TSecond, TKey> secondKey, 
    Action<TFirst, IEnumerable<TSecond>> addChildren
    )
{
    var first = reader.Read<TFirst>().ToList();
    var childMap = reader
        .Read<TSecond>()
        .GroupBy(s => secondKey(s))
        .ToDictionary(g => g.Key, g => g.AsEnumerable());

    foreach (var item in first)
    {
        IEnumerable<TSecond> children;
        if(childMap.TryGetValue(firstKey(item), out children))
        {
            addChildren(item,children);
        }
    }

    return first;
}

これは少しトリッキーで複雑なので、注意が必要です。私はこれをコアに含めることに傾倒していません。

于 2011-06-17T02:23:19.290 に答える
33

参考までに-次のことを行うことで、サムの答えが機能しました。

まず、「Extensions.cs」というクラス ファイルを追加しました。次の 2 か所で、「this」キーワードを「reader」に変更する必要がありました。

using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;

namespace TestMySQL.Helpers
{
    public static class Extensions
    {
        public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
            (
            this Dapper.SqlMapper.GridReader reader,
            Func<TFirst, TKey> firstKey,
            Func<TSecond, TKey> secondKey,
            Action<TFirst, IEnumerable<TSecond>> addChildren
            )
        {
            var first = reader.Read<TFirst>().ToList();
            var childMap = reader
                .Read<TSecond>()
                .GroupBy(s => secondKey(s))
                .ToDictionary(g => g.Key, g => g.AsEnumerable());

            foreach (var item in first)
            {
                IEnumerable<TSecond> children;
                if (childMap.TryGetValue(firstKey(item), out children))
                {
                    addChildren(item, children);
                }
            }

            return first;
        }
    }
}

次に、最後のパラメーターを変更して、次のメソッドを追加しました。

public IEnumerable<Contact> GetContactsAndPhoneNumbers()
{
    var sql = @"
SELECT * FROM Contacts WHERE clientid=1
SELECT * FROM Phone where ContactId in (select ContactId FROM Contacts WHERE clientid=1)";

    using (var connection = GetOpenConnection())
    {
        var mapped = connection.QueryMultiple(sql)    
            .Map<Contact,Phone, int>     (        
            contact => contact.ContactID,        
            phone => phone.ContactID,
            (contact, phones) => { contact.Phones = phones; }      
        ); 
        return mapped;
    }
}
于 2012-02-08T02:02:49.667 に答える
26

https://www.tritac.com/blog/dappernet-by-example/をご覧ください。 次のようなことができます。

public class Shop {
  public int? Id {get;set;}
  public string Name {get;set;}
  public string Url {get;set;}
  public IList<Account> Accounts {get;set;}
}

public class Account {
  public int? Id {get;set;}
  public string Name {get;set;}
  public string Address {get;set;}
  public string Country {get;set;}
  public int ShopId {get;set;}
}

var lookup = new Dictionary<int, Shop>()
conn.Query<Shop, Account, Shop>(@"
                  SELECT s.*, a.*
                  FROM Shop s
                  INNER JOIN Account a ON s.ShopId = a.ShopId                    
                  ", (s, a) => {
                       Shop shop;
                       if (!lookup.TryGetValue(s.Id, out shop)) {
                           lookup.Add(s.Id, shop = s);
                       }
                       shop.Accounts.Add(a);
                       return shop;
                   },
                   ).AsQueryable();
var resultList = lookup.Values;

dapper.net テストからこれを取得しました: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

于 2013-04-16T13:13:39.063 に答える
12

複数の結果セットのサポート

あなたの場合、複数の結果セットクエリを使用する方がはるかに優れています(そして簡単です)。これは単に、次の 2 つの select ステートメントを記述する必要があることを意味します。

  1. 連絡先を返すもの
  2. そして、電話番号を返すもの

このようにして、オブジェクトは一意になり、重複しません。

于 2011-06-17T12:53:41.927 に答える
11

これは、非常に使いやすい再利用可能なソリューションです。Andrews answerを少し修正したものです。

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

使用例

public class Contact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<Phone> Phones { get; set; } // must be IList

    public Contact()
    {
        this.Phones = new List<Phone>(); // POCO is responsible for instantiating child list
    }
}

public class Phone
{
    public int PhoneID { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
    public string Type { get; set; }
    public bool IsActive { get; set; }
}

conn.QueryParentChild<Contact, Phone, int>(
    "SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID",
    contact => contact.ContactID,
    contact => contact.Phones,
    splitOn: "PhoneId");
于 2014-03-14T20:56:31.223 に答える
2

この問題に対する私の解決策を共有し、私が使用したアプローチについて誰かが建設的なフィードバックを持っているかどうかを確認したかったのですか?

私が取り組んでいるプロジェクトには、最初に説明する必要があるいくつかの要件があります。

  1. これらのクラスは API ラッパーでパブリックに共有されるため、POCO をできるだけクリーンに保つ必要があります。
  2. 上記の要件により、私の POCO は別のクラス ライブラリにあります。
  3. データによって異なる複数のオブジェクト階層レベルが存在する予定です (そのため、ジェネリック型マッパーを使用できないか、考えられるすべての事態に対応するために大量のオブジェクトを作成する必要があります)。

したがって、私が行ったことは、次のように元の行の列として単一の JSON 文字列を返すことにより、SQL が 2 番目から n 番目のレベルの階層を処理するようにすることです (説明するために他の列/プロパティなどを取り除きます)。

Id  AttributeJson
4   [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]

次に、私の POCO は以下のように構築されます。

public abstract class BaseEntity
{
    [KeyAttribute]
    public int Id { get; set; }
}

public class Client : BaseEntity
{
    public List<ClientAttribute> Attributes{ get; set; }
}
public class ClientAttribute : BaseEntity
{
    public string Name { get; set; }
    public string Value { get; set; }
}

POCO が BaseEntity から継承する場所。(説明するために、クライアント オブジェクトの「属性」プロパティで示されるように、かなり単純な単一レベルの階層を選択しました。)

次に、 POCO から継承する次の「データクラス」をデータレイヤーに持っていますClient

internal class dataClient : Client
{
    public string AttributeJson
    {
        set
        {
            Attributes = value.FromJson<List<ClientAttribute>>();
        }
    }
}

上記のように、SQL はAttributeJsondataClient クラスのプロパティにマップされた "AttributeJson" という列を返します。Attributesこれには、JSON を継承されたClientクラスのプロパティに逆シリアル化するセッターしかありません。dataClient クラスはinternalデータ アクセス レイヤーにあり、ClientProvider(私のデータ ファクトリ) は元のクライアント POCO を呼び出し元のアプリ/ライブラリに次のように返します。

var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();

私は Dapper.Contrib を使用しておりGet<T>、返す新しいメソッドを追加したことに注意してくださいIEnumerable<T>

このソリューションには、注意すべき点がいくつかあります。

  1. JSONシリアライゼーションには明らかなパフォーマンスのトレードオフがあります.2つのサブプロパティを持つ1050行に対してこれをベンチマークしList<T>、それぞれがリストに2つのエンティティを持ち、279ミリ秒でクロックインしました-これは私のプロジェクトのニーズに受け入れられます-これもSQL 側のゼロ最適化なので、数ミリ秒短縮できるはずです。

  2. これは、必要なプロパティごとに JSON を構築するために追加の SQL クエリが必要であることを意味しますがList<T>、SQL をよく知っていて、ダイナミクスやリフレクションなどにあまり精通していないので、これは私に合っています。ボンネットの下で何が起こっているのかを実際に理解しているので、物事をより細かく制御できます:-)

これよりも優れた解決策があるかもしれません。もしあれば、あなたの考えを聞いていただければ幸いです。 )。

于 2013-03-05T07:48:53.150 に答える
-1

これを使って:

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public Category Category { get; set; }
}
public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
    public ICollection<Product> Products { get; set; }
}

        using (var connection = new SQLiteConnection(connString))
        {
            var sql = @"select productid, productname, p.categoryid, categoryname 
            from products p 
            inner join categories c on p.categoryid = c.categoryid";
            var products = await connection.QueryAsync<Product, Category, Product>(sql, (product, category) => {
                product.Category = category;
                return product;
            },
            splitOn: "CategoryId");
            products.ToList().ForEach(product => Console.WriteLine($"Product: {product.ProductName}, Category: {product.Category.CategoryName}"));
            Console.ReadLine();
        }

出典:関係の管理

于 2021-04-09T12:29:44.923 に答える