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

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.