Мульти-Сопоставитель для создания иерархии объектов


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

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

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

Я хотел бы в конечном итоге с чем-то, что возвращает свяжитесь с несколькими объектами телефона. Таким образом, если бы у меня было 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 ' к типу - Система.Коллекции.Родовой.IEnumerable`1 [Телефон]'."

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 72

7 ответов:

вы не делаете ничего плохого, это просто не так, как был разработан API. Все это Query API-интерфейсов всегда возвращает объект для каждой строки базы данных.

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

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

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

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

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

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

во-первых, я добавил файл класса под названием "расширения.цезий." Мне пришлось изменить ключевое слово "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;
    }
}

проверить 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

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

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

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

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

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

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

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

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 может быть вызвана снова для следующего дочернего объекта, используя тот же родительский объект. Вы также можете реализовать расщепляет на родительской или дочерней инструкции чтения независимо от функции map.

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

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

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

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

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

тогда, мой Поко строятся, как показано ниже :

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. (для иллюстрации я выбрал довольно простой, одноуровневый heirarchy, как показано в свойстве "атрибуты" объекта клиента. )

у меня тогда есть в моем слое данных следующий "класс данных", который наследует от 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 (my data factory) возвращает исходный клиент POCO в вызывающее приложение / библиотеку следующим образом:

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

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

есть пара вещей, чтобы отметить с этим решением:

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

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

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