Multi-Mapper для создания иерархии объектов

82

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

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

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

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

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

Джорин
источник

Ответы:

69

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

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

Здесь есть 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;
}

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

Сэм Шафран
источник
Очень круто. У этой штуки довольно много мощности ... Я думаю, она просто привыкает к тому, как ею пользоваться. Я изучу полезную нагрузку своих запросов и посмотрю, насколько велики наборы результатов, и посмотрю, можем ли мы позволить себе иметь несколько запросов и сопоставлять их вместе.
Jorin
@Jorin, другой вариант - организовать несколько подключений и объединить результаты. Это немного сложнее.
Sam Saffron,
1
Я бы также добавил else после if (childMap.TryGetvalue (..)), чтобы дочерняя коллекция по умолчанию инициализировалась пустой коллекцией вместо NULL, если дочерних элементов нет. Как это: else {addChildren (item, new TChild [] {}); }
Мариус
1
@SamSaffron Я люблю Dapper. Спасибо. Однако у меня есть вопрос. Один ко многим - обычное явление в запросах SQL. Что вы планировали использовать при проектировании? Я хочу сделать это способом Dapper, но в данный момент я использую SQL. Как я думаю об этом, исходящем из SQL, где одна сторона обычно является «драйвером». Почему в Dapper так много сторон? Дело в том, что мы получаем объект и выполняем парсинг постфактум? Спасибо за отличную библиотеку.
джонни
2
Убедитесь, что вы используете правильный инструмент для работы. Если у вас нет серьезных требований к производительности базы данных или вы не тестировали свою систему, вы потратили впустую часы или, возможно, дни своей жизни, используя Dapper.
Алуан Хаддад
32

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

Сначала я добавил файл класса под названием «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;
    }
}
Майк Глисон
источник
24

Посетите 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

Йерун К.
источник
2
Вот Это Да! Для меня это было самым простым решением. Конечно, для одного-> многих (при условии двух таблиц) я бы выбрал двойной выбор. Однако в моем случае у меня есть one-> one-> many, и это отлично работает. Теперь он действительно возвращает много избыточных данных, но в моем случае эта избыточность относительно мала - в лучшем случае 10 строк.
code5
Это хорошо работает для двух уровней, но становится сложнее, когда у вас их больше.
Самир Агияр,
1
Если дочерние данные отсутствуют, код (s, a) будет вызываться с a = null, а Accounts будет содержать список с нулевой записью вместо того, чтобы быть пустым. Вам нужно добавить «if (a! = Null)» перед «shop.Accounts.Add (a)»
Этьен Чарланд
12

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

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

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

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

Роберт Коритник
источник
1
Хотя другие ответы могут быть по-своему элегантными, мне, как правило, нравится этот, потому что код легче рассуждать. Я могу построить иерархию на несколько уровней глубиной с помощью нескольких операторов select и примерно 30 строк кода foreach / linq. Это может сломаться с массивными наборами результатов, но, к счастью, у меня нет этой проблемы (пока).
Сэм Стори
10

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

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");
Глина
источник
7

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

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

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

Вот дополнительный метод расширения от одного до 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;
    }
shlgug
источник
2
Спасибо за это - отличное решение. удалили оператор if, чтобы вместо того, чтобы не вызывать addChilder ни для каких дочерних элементов, вызывающая функция могла обрабатывать значения NULL. Так я могу добавлять пустые списки, с которыми намного проще работать.
Младен Михайлович
1
Это фантастическое решение. У меня были проблемы с "динамическим поиском". Это можно решить с помощью этого contactList = multi.MapChild <Contact, Phone, int> (/ * тот же код, что и здесь * /
granadaCoder
4

Однажды мы решили переместить наш DataAccessLayer в хранимые процедуры, и эти процедуры часто возвращают несколько связанных результатов (пример ниже).

Что ж, мой подход почти такой же, но, может быть, немного удобнее.

Вот как может выглядеть ваш код:

using ( var conn = GetConn() )
{
    var res = await conn
        .StoredProc<Person>( procName, procParams )
        .Include<Book>( ( p, b ) => p.Books = b.Where( x => x.PersonId == p.Id ).ToList() )
        .Include<Course>( ( p, c ) => p.Courses = c.Where( x => x.PersonId == p.Id ).ToList() )
        .Include<Course, Mark>( ( c, m ) => c.Marks = m.Where( x => x.CourseId == c.Id ).ToList() )
        .Execute();
}


Давайте разберемся ...

Расширение:

public static class SqlExtensions
{
    public static StoredProcMapper<T> StoredProc<T>( this SqlConnection conn, string procName, object procParams )
    {
        return StoredProcMapper<T>
            .Create( conn )
            .Call( procName, procParams );
    }
}

Картограф:

public class StoredProcMapper<T>
{
    public static StoredProcMapper<T> Create( SqlConnection conn )
    {
        return new StoredProcMapper<T>( conn );
    }

    private List<MergeInfo> _merges = new List<MergeInfo>();

    public SqlConnection Connection { get; }
    public string ProcName { get; private set; }
    public object Parameters { get; private set; }

    private StoredProcMapper( SqlConnection conn )
    {
        Connection = conn;
        _merges.Add( new MergeInfo( typeof( T ) ) );
    }

    public StoredProcMapper<T> Call( object procName, object parameters )
    {
        ProcName = procName.ToString();
        Parameters = parameters;

        return this;
    }

    public StoredProcMapper<T> Include<TChild>( MergeDelegate<T, TChild> mapper )
    {
        return Include<T, TChild>( mapper );
    }

    public StoredProcMapper<T> Include<TParent, TChild>( MergeDelegate<TParent, TChild> mapper )
    {
        _merges.Add( new MergeInfo<TParent, TChild>( mapper ) );
        return this;
    }

    public async Task<List<T>> Execute()
    {
        if ( string.IsNullOrEmpty( ProcName ) )
            throw new Exception( $"Procedure name not specified! Please use '{nameof(Call)}' method before '{nameof( Execute )}'" );

        var gridReader = await Connection.QueryMultipleAsync( 
            ProcName, Parameters, commandType: CommandType.StoredProcedure );

        foreach ( var merge in _merges )
        {
            merge.Result = gridReader
                .Read( merge.Type )
                .ToList();
        }

        foreach ( var merge in _merges )
        {
            if ( merge.ParentType == null )
                continue;

            var parentMerge = _merges.FirstOrDefault( x => x.Type == merge.ParentType );

            if ( parentMerge == null )
                throw new Exception( $"Wrong parent type '{merge.ParentType.FullName}' for type '{merge.Type.FullName}'." );

            foreach ( var parent in parentMerge.Result )
            {
                merge.Merge( parent, merge.Result );
            }
        }

        return _merges
            .First()
            .Result
            .Cast<T>()
            .ToList();
    }

    private class MergeInfo
    {
        public Type Type { get; }
        public Type ParentType { get; }
        public IEnumerable Result { get; set; }

        public MergeInfo( Type type, Type parentType = null )
        {
            Type = type;
            ParentType = parentType;
        }

        public void Merge( object parent, IEnumerable children )
        {
            MergeInternal( parent, children );
        }

        public virtual void MergeInternal( object parent, IEnumerable children )
        {

        }
    }

    private class MergeInfo<TParent, TChild> : MergeInfo
    {
        public MergeDelegate<TParent, TChild> Action { get; }

        public MergeInfo( MergeDelegate<TParent, TChild> mergeAction )
            : base( typeof( TChild ), typeof( TParent ) )
        {
            Action = mergeAction;
        }

        public override void MergeInternal( object parent, IEnumerable children )
        {
            Action( (TParent)parent, children.Cast<TChild>() );
        }
    }

    public delegate void MergeDelegate<TParent, TChild>( TParent parent, IEnumerable<TChild> children );
}

Это все, но если вы хотите провести быстрый тест, вот модели и процедуры для вас:

Модели:

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; }

    public List<Course> Courses { get; set; }
    public List<Book> Books { get; set; }

    public override string ToString() => Name;
}

public class Book
{
    public Guid Id { get; set; }
    public Guid PersonId { get; set; }
    public string Name { get; set; }

    public override string ToString() => Name;
}

public class Course
{
    public Guid Id { get; set; }
    public Guid PersonId { get; set; }
    public string Name { get; set; }

    public List<Mark> Marks { get; set; }

    public override string ToString() => Name;
}

public class Mark
{
    public Guid Id { get; set; }
    public Guid CourseId { get; set; }
    public int Value { get; set; }

    public override string ToString() => Value.ToString();
}

СП:

if exists ( 
    select * 
    from sysobjects 
    where  
        id = object_id(N'dbo.MultiTest')
        and ObjectProperty( id, N'IsProcedure' ) = 1 )
begin
    drop procedure dbo.MultiTest
end
go

create procedure dbo.MultiTest
    @PersonId UniqueIdentifier
as
begin

    declare @tmpPersons table 
    (
        Id UniqueIdentifier,
        Name nvarchar(50)
    );

    declare @tmpBooks table 
    (
        Id UniqueIdentifier,
        PersonId UniqueIdentifier,
        Name nvarchar(50)
    )

    declare @tmpCourses table 
    (
        Id UniqueIdentifier,
        PersonId UniqueIdentifier,
        Name nvarchar(50)
    )

    declare @tmpMarks table 
    (
        Id UniqueIdentifier,
        CourseId UniqueIdentifier,
        Value int
    )

--------------------------------------------------

    insert into @tmpPersons
    values
        ( '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Иван' ),
        ( '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Василий' ),
        ( '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Алефтина' )


    insert into @tmpBooks
    values
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Математика' ),
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Физика' ),
        ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Геометрия' ),

        ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Биология' ),
        ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Химия' ),

        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга История' ),
        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Литература' ),
        ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Древне-шумерский диалект иврита' )


    insert into @tmpCourses
    values
        ( '30945b68-a6ef-4da8-9a35-d3b2845e7de3', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Математика' ),
        ( '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Физика' ),
        ( '92bbefd1-9fec-4dc7-bb58-986eadb105c8', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Геометрия' ),

        ( '923a2f0c-c5c7-4394-847c-c5028fe14711', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Биология' ),
        ( 'ace50388-eb05-4c46-82a9-5836cf0c988c', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Химия' ),

        ( '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'История' ),
        ( '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Литература' ),
        ( '73ac366d-c7c2-4480-9513-28c17967db1a', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Древне-шумерский диалект иврита' )

    insert into @tmpMarks
    values
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 98 ),
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 87 ),
        ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 76 ),

        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 89 ),
        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 78 ),
        ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 67 ),

        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 79 ),
        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 68 ),
        ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 75 ),
        ----------
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 198 ),
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 187 ),
        ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 176 ),

        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 189 ),
        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 178 ),
        ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 167 ),
        ----------
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 8 ),
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 7 ),
        ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 6 ),

        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 9 ),
        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 8 ),
        ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 7 ),

        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 9 ),
        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 8 ),
        ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 5 )

--------------------------------------------------

    select * from @tmpPersons
    select * from @tmpBooks
    select * from @tmpCourses
    select * from @tmpMarks

end
go
Сэм Ш
источник
1
Я не знаю, почему этот подход до сих пор не получил внимания или комментариев, но я считаю его очень интересным и логически структурированным. Спасибо, что поделился. Я думаю, вы можете применить этот подход и к функциям с табличным значением или даже к строкам SQL - они просто отличаются типом команды. Просто некоторые расширения / перегрузки, и это должно работать для всех распространенных типов запросов.
Гримм
чтобы убедиться, что я правильно это прочитал, это требует, чтобы пользователь точно знал, в каком порядке будет процедура возврата результатов, верно? Если вы поменяли местами, например, Include <Book> и Include <Course>, это вызовет?
cubesnyc
@cubesnyc, я не помню, выбрасывает ли он, но да, пользователь должен знать порядок
Сэм Ш.
2

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

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

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

Итак, что я сделал, чтобы получить SQL для обработки 2 - го - го уровня, возвращая иерархию строки Single 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. (Для иллюстрации я выбрал довольно простую одноуровневую иерархию, как показано свойством «Attributes» клиентского объекта.)

Затем на моем уровне данных имеется следующий «класс данных», который наследуется от 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 мс - что приемлемо для потребностей моих проектов - это также с НУЛЕВАЯ оптимизация на стороне SQL, поэтому я смогу сэкономить несколько мс.

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

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

Дэйв Лонг
источник
Это интересно. Есть ли шанс, что вы можете поделиться частью SQL?
WhiteRuski