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

Я хотів би закінчити щось, що повертає контакт із кількома об'єктами телефону. Таким чином, якби я мав 2 контакти, по 2 телефони в кожному, мій SQL повертав би об’єднання тих, як результат, набір з 4 рядками. Тоді Даппер видав би 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 [Телефон]'."

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, за винятком того, що я переходжу від батьків до дитини, а не від дитини до батьків.

Спасибі заздалегідь

Відповіді:


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
Я також додав би інше після if (childMap.TryGetvalue (..)), щоб дочірня колекція за замовчуванням ініціалізувалася до порожньої колекції замість NULL, якщо немає дочірніх елементів. Ось так: else {addChildren (item, new TChild [] {}); }
Маріус

1
@SamSaffron Я люблю Даппера. Дякую. У мене все-таки є питання. Один-до-багатьох є загальним явищем у запитах SQL. Що ви мали на увазі під час проектування для реалізатора? Я хочу зробити це за допомогою Dapper, але на даний момент я в SQL. Як я думаю про це з SQL, де One Side, як правило, є "драйвером". Чому сторона Many в Dapper? Суть у тому, що ми отримуємо об’єкт і робимо синтаксичний розбір після факту? Дякую за чудову бібліотеку.
Джонні,

2
Переконайтеся, що ви використовуєте правильний інструмент для роботи. Якщо ви не маєте значних вимог до продуктивності бази даних або не встановили порівняльну оцінку своєї системи, ви витратили години або, можливо, дні свого життя, використовуючи Dapper.
Алуан Хаддад

32

FYI - Я отримав відповідь Сема, працюючи наступним чином:

Спочатку я додав файл класу під назвою "Extensions.cs". Мені довелося змінити ключове слово "це" на "читач" у двох місцях:

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
Оце Так! Для мене це було найпростішим рішенням. Звичайно, для одного-> багатьох, (припускаючи дві таблиці), я б пішов з подвійним вибором. Однак у моєму випадку у мене є одне-> одне-> багато, і це чудово працює. Зараз він повертає багато зайвих даних, але для мого випадку ця надмірність є відносно невеликою - в кращому випадку 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;
}

Потім функцію map можна знову викликати для наступного дочірнього об'єкта, використовуючи той самий батьківський об'єкт. Ви також можете реалізувати розбиття на батьківських або дочірніх операторах читання незалежно від функції 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;
    }

2
Дякую за це - чудове рішення. вилучив оператор if, щоб замість того, щоб не викликати addChilder без дочірніх елементів, функція виклику могла обробляти нулі. Таким чином я можу додати порожні списки, з якими набагато простіше працювати.
Младен Михайлович

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, я не пам’ятаю, якщо це кине, але так, користувач повинен знати порядок
Sam Sch

2

Я хотів поділитися своїм рішенням цього питання і подивитися, чи є у когось конструктивні відгуки щодо підходу, який я використав?

У мене в проекті, над яким я працюю, є кілька вимог, які я повинен пояснити спочатку:

  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 мс - що прийнятно для потреб моїх проектів - це також з НУЛЬОВА оптимізація на стороні SQL, тому я мав би змогу поголити там кілька мс.

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

Можливо, буде краще рішення, ніж це, і якщо воно буде, я б дуже вдячний почути ваші думки - це саме те рішення, яке я придумав, і яке відповідає моїм потребам у цьому проекті (хоча це експериментальне на етапі розміщення) ).


Це цікаво. Будь-який шанс ви можете поділитися частиною SQL?
WhiteRuski
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.