Multi-Mapper для создания иерархии объектов - PullRequest
72 голосов
/ 17 июня 2011

Я немного поигрался с этим, потому что кажется, что он похож на документально подтвержденный пример постов / пользователей , но немного отличается и не работает для меня.

При условии следующей упрощенной настройки (у контакта есть несколько телефонных номеров):

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.Таким образом, если бы у меня было 2 контакта, по 2 телефона в каждом, мой SQL возвращал бы соединение из них в виде результирующего набора с 4 строками.Затем Dapper выскочит 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);

Я что-то не так делаю?Это похоже на пример posts / owner, за исключением того, что я иду от родителя к ребенку, а не от ребенка к родителю.

Заранее спасибо

Ответы [ 7 ]

63 голосов
/ 17 июня 2011

Вы не делаете ничего плохого, это просто не тот способ, которым был разработан API. Все Query API будут всегда возвращать объект на строку базы данных.

Итак, это хорошо работает для многих -> в одном направлении, но менее хорошо для одного -> для нескольких карт.

Здесь есть 2 вопроса:

  1. Если мы представим встроенный преобразователь, который работает с вашим запросом, мы, как ожидается, "отбросим" дублирующиеся данные. (Контакты. * Дублируется в вашем запросе)

  2. Если мы разработаем его для работы с парой один -> много, нам понадобится какая-то карта идентичности. Что добавляет сложности.


Возьмем, к примеру, этот запрос, который эффективен, если вам просто нужно извлечь ограниченное количество записей, если вы увеличиваете этот объем до миллиона, становится сложнее, потому что вам нужно выполнять потоковую передачу и не можете загрузить все в память:

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

Так как это немного сложно и сложно, с оговорками. Я не склоняюсь к тому, чтобы включить это в ядро.

30 голосов
/ 08 февраля 2012

К вашему сведению - я получил ответ Сэма, выполнив следующее:

Сначала я добавил файл класса с именем "Extensions.cs".Мне пришлось изменить ключевое слово "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;
    }
}
20 голосов
/ 16 апреля 2013

Выезд 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

10 голосов
/ 15 марта 2014

Вот многоразовое решение, которое довольно просто в использовании. Это небольшая модификация ответа Эндрюса .

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");
10 голосов
/ 17 июня 2011

Поддержка нескольких результирующих наборов

В вашем случае было бы намного лучше (и также проще) иметь запрос с несколькими результирующими наборами.Это просто означает, что вы должны написать два оператора выбора:

  1. Один, который возвращает контакты
  2. И другой, который возвращает их номера телефонов

Таким образом, ваши объектыбудет уникальным и не будет дублировать.

7 голосов
/ 29 июля 2016

Основываясь на подходе Сэма Шафрона (и Майка Глисона), вот решение, которое позволит использовать несколько детей и несколько уровней.

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

namespace TestMySQL.Helpers
{
    public static class Extensions
    {
        public static IEnumerable<TFirst> MapChild<TFirst, TSecond, TKey>
            (
            this SqlMapper.GridReader reader,
            List<TFirst> parent,
            List<TSecond> child,
            Func<TFirst, TKey> firstKey,
            Func<TSecond, TKey> secondKey,
            Action<TFirst, IEnumerable<TSecond>> addChildren
            )
        {
            var childMap = child
                .GroupBy(secondKey)
                .ToDictionary(g => g.Key, g => g.AsEnumerable());
            foreach (var item in parent)
            {
                IEnumerable<TSecond> children;
                if (childMap.TryGetValue(firstKey(item), out children))
                {
                    addChildren(item, children);
                }
            }
            return parent;
        }
    }
}

Тогда вы сможете прочитать его вне функции.

using (var multi = conn.QueryMultiple(sql))
{
    var contactList = multi.Read<Contact>().ToList();
    var phoneList = multi.Read<Phone>().ToList;
    contactList = multi.MapChild
        (
            contactList,
            phoneList,
            contact => contact.Id, 
            phone => phone.ContactId,
            (contact, phone) => {contact.Phone = phone;}
        ).ToList();
    return contactList;
}

Затем можно снова вызвать функцию map для следующего дочернего объекта, используя тот же родительский объект.Вы также можете реализовать split в выражениях родительского или дочернего чтения независимо от функции map.

Вот дополнительный метод расширения «от одного до N»

    public static TFirst MapChildren<TFirst, TSecond, TKey>
        (
        this SqlMapper.GridReader reader,
        TFirst parent,
        IEnumerable<TSecond> children,
        Func<TFirst, TKey> firstKey,
        Func<TSecond, TKey> secondKey,
        Action<TFirst, IEnumerable<TSecond>> addChildren
        )
    {
        if (parent == null || children == null || !children.Any())
        {
            return parent;
        }

        Dictionary<TKey, IEnumerable<TSecond>> childMap = children
            .GroupBy(secondKey)
            .ToDictionary(g => g.Key, g => g.AsEnumerable());

        if (childMap.TryGetValue(firstKey(parent), out IEnumerable<TSecond> foundChildren))
        {
            addChildren(parent, foundChildren);
        }

        return parent;
    }
1 голос
/ 05 марта 2013

Я хотел бы поделиться своим решением этой проблемы и посмотреть, есть ли у кого-нибудь конструктивные отзывы о подходе, который я использовал?

У меня есть несколько требований в проекте, над которым я работаю, и мне нужно сначала объяснить:

  1. Я должен держать свои POCO как можно более чистыми, так как эти классы будут публично предоставлены в обертке API.
  2. Мои POCO находятся в отдельной библиотеке классов из-за вышеуказанного требования
  3. Будет несколько уровней иерархии объектов, которые будут варьироваться в зависимости от данных (поэтому я не могу использовать средство отображения универсального типа, или мне придется написать тонны из них, чтобы удовлетворить все возможные случаи)

Итак, я сделал так, чтобы SQL обрабатывал иерархию 2-го уровня, возвращая одну строку JSON в виде столбца в исходной строке следующим образом ( удалил другие столбцы / свойства и т. Д., Чтобы проиллюстрировать это ):

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 возвращает столбец с именем "AttributeJson", который сопоставляется со свойством AttributeJson в классе dataClient. У него есть только установщик, который десериализует JSON в свойство Attributes унаследованного класса Client. Класс dataClient для уровня доступа к данным равен internal, а ClientProvider (моя фабрика данных) возвращает исходный клиентский POCO вызывающему приложению / библиотеке следующим образом:

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

Обратите внимание, что я использую Dapper.Contrib и добавил новый Get<T> метод, который возвращает IEnumerable<T>

В этом решении есть пара моментов, на которые следует обратить внимание:

  1. Есть очевидный компромисс производительности с сериализацией JSON - я сравнил это с 1050 строками с 2 вложенными List<T> свойствами, у каждого из которых есть 2 объекта в списке, и он работает с 279 мс - что является приемлемым для нужд моего проекта - это также с ZERO-оптимизацией на стороне SQL, поэтому я смогу побриться там на несколько мс.

  2. Это означает, что для создания JSON для каждого требуемого свойства List<T> требуются дополнительные запросы SQL, но, опять же, мне это подходит, поскольку я довольно хорошо знаю SQL и не очень хорошо разбираюсь в динамике / отражении и т. Д. таким образом, я чувствую, что у меня больше контроля над вещами, поскольку я действительно понимаю, что происходит под капотом: -)

Возможно, найдется лучшее решение, чем это, и если оно будет, я был бы очень признателен, если бы вы услышали ваши мысли - это только то решение, которое я придумал, которое до сих пор соответствует моим потребностям в этом проекте (хотя это экспериментально на этап размещения).

...