Multi-Mapper do tworzenia hierarchii obiektów

82

Bawiłem się tym przez chwilę, ponieważ wydaje się, że wygląda to bardzo podobnie do udokumentowanego przykładu postów / użytkowników , ale jest nieco inny i nie działa dla mnie.

Zakładając następującą uproszczoną konfigurację (kontakt ma wiele numerów telefonów):

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

Chciałbym skończyć z czymś, co zwraca Contact z wieloma obiektami Phone. W ten sposób, gdybym miał 2 kontakty, każdy z 2 telefonami, mój SQL zwróciłby łączenie tych kontaktów jako zestaw wynikowy z łącznymi 4 wierszami. Następnie Dapper wyskoczyłby 2 obiekty kontaktowe z dwoma telefonami w każdym.

Oto kod SQL w procedurze składowanej:

SELECT *
FROM Contacts
    LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1

Próbowałem tego, ale skończyło się na 4 krotkach (co jest w porządku, ale nie to, na co liczyłem ... oznacza to tylko, że nadal muszę ponownie znormalizować wynik):

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

a kiedy próbuję innej metody (poniżej), pojawia się wyjątek „Nie można rzutować obiektu typu„ System.Int32 ”na typ„ 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);

Czy ja po prostu robię coś złego? Wygląda to tak, jak przykład postów / właściciela, z tym wyjątkiem, że przechodzę od rodzica do dziecka zamiast z dziecka do rodzica.

Z góry dziękuję

Jorin
źródło

Odpowiedzi:

69

Nie robisz nic złego, po prostu nie jest to sposób, w jaki zaprojektowano API. Wszystkie Queryinterfejsy API zawsze zwracają obiekt na wiersz bazy danych.

Tak więc działa to dobrze w wielu -> jednym kierunku, ale gorzej w jednym -> wielu multi-mapach.

Istnieją 2 problemy:

  1. Jeśli wprowadzimy wbudowany program mapujący, który działa z Twoim zapytaniem, powinniśmy „odrzucić” zduplikowane dane. (Kontakty. * Są zduplikowane w zapytaniu)

  2. Jeśli zaprojektujemy go do pracy z parą jeden -> wiele, będziemy potrzebować jakiejś mapy tożsamości. Co zwiększa złożoność.


Weźmy na przykład to zapytanie, które jest wydajne, jeśli potrzebujesz tylko pobrać ograniczoną liczbę rekordów, jeśli przesuniesz to do miliona rzeczy, stanie się trudniejsze, ponieważ musisz przesyłać strumieniowo i nie możesz załadować wszystkiego do pamięci:

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

Co możesz zrobić, to rozszerzyć, GridReaderaby umożliwić ponowne mapowanie:

var mapped = cnn.QueryMultiple(sql)
   .Map<Contact,Phone, int>
    (
       contact => contact.ContactID, 
       phone => phone.ContactID,
       (contact, phones) => { contact.Phones = phones };  
    );

Zakładając, że rozszerzysz swój GridReader i wykorzystasz mappera:

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

Ponieważ jest to nieco skomplikowane i skomplikowane, z zastrzeżeniami. Nie skłaniam się do włączenia tego do rdzenia.

Sam Saffron
źródło
Bardzo fajny. Ta rzecz ma sporo mocy ... Myślę, że po prostu przyzwyczaja się do tego, jak jej używać. Przyjrzę się zawartości moich zapytań i zobaczę, jak duże są zestawy wyników i zobaczę, czy stać nas na wiele zapytań i zmapować je razem.
Jorin
@Jorin, inną opcją byłoby zorganizowanie wielu połączeń i splot wyników. To trochę trudniejsze.
Sam Saffron
1
Dodałbym również else po if (childMap.TryGetvalue (..)), aby kolekcja podrzędna była domyślnie inicjowana jako pusta kolekcja zamiast NULL, jeśli nie ma elementów podrzędnych. W ten sposób: else {addChildren (element, nowy TChild [] {}); }
Marius
1
@SamSaffron Uwielbiam Dapper. Dziękuję Ci. Mam jednak pytanie. W zapytaniach SQL często występuje „jeden do wielu”. Co podczas projektowania miałeś na myśli, aby zastosować go osoba wdrażająca? Chcę to zrobić w elegancki sposób, ale w tej chwili jestem w stylu SQL. Jak myślę o tym pochodzącym z SQL, gdzie One Side jest zwykle „sterownikiem”. Dlaczego strona Wielu jest tak elegancka? Czy chodzi o to, abyśmy otrzymali obiekt i przeprowadzili analizę po fakcie? Dzięki za wspaniałą bibliotekę.
johnny
2
Upewnij się, że używasz odpowiedniego narzędzia do pracy. Jeśli nie masz ogromnych wymagań dotyczących wydajności bazy danych lub nie wykonałeś testów porównawczych swojego systemu, straciłeś godziny lub dni swojego życia, używając Dapper.
Aluan Haddad
32

FYI - otrzymałem odpowiedź Sama, wykonując następujące czynności:

Najpierw dodałem plik klasy o nazwie „Extensions.cs”. Musiałem zmienić słowo kluczowe „to” na „czytelnik” w dwóch miejscach:

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

Po drugie dodałem następującą metodę, modyfikując ostatni parametr:

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;
    }
}
Mike Gleason
źródło
24

Sprawdź https://www.tritac.com/blog/dappernet-by-example/ Możesz zrobić coś takiego:

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;

Mam to z testów dapper.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

Jeroen K.
źródło
2
Łał! Dla mnie to najłatwiejsze rozwiązanie. To prawda, dla jednego -> wielu (zakładając dwie tabele), wybrałbym podwójną selekcję. Jednak w moim przypadku mam jeden-> jeden-> wiele i to działa świetnie. Teraz przywraca wiele nadmiarowych danych, ale w moim przypadku ta nadmiarowość jest stosunkowo niewielka - w najlepszym przypadku 10 wierszy.
code5
Działa to dobrze na dwóch poziomach, ale staje się trudne, gdy masz więcej.
Samir Aguiar
1
Jeśli nie ma danych podrzędnych, kod (s, a) zostanie wywołany z a = null, a konta będą zawierały listę z pustym wpisem zamiast pustego. Musisz dodać „if (a! = Null)” przed „shop.Accounts.Add (a)”
Etienne Charland
12

Obsługa wielu zestawów wyników

W twoim przypadku znacznie lepiej (i łatwiej) byłoby mieć zapytanie z wieloma wynikami. Oznacza to po prostu, że powinieneś napisać dwie instrukcje Select:

  1. Taka, która zwraca kontakty
  2. I taki, który zwraca ich numery telefonów

W ten sposób Twoje obiekty byłyby unikalne i nie powielałyby się.

Robert Koritnik
źródło
1
Podczas gdy inne odpowiedzi mogą być eleganckie na swój sposób, to lubię tę, ponieważ kod jest łatwiejszy do zrozumienia. Potrafię zbudować hierarchię, która jest głęboka na kilka poziomów z kilkoma instrukcjami select i około 30 wierszami kodu foreach / linq. Może się to zepsuć przy ogromnych zestawach wyników, ale na szczęście (jeszcze) nie mam tego problemu.
Sam Storie
10

Oto rozwiązanie wielokrotnego użytku, które jest dość łatwe w użyciu. Jest to niewielka modyfikacja odpowiedzi Andrewsa .

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

Przykładowe użycie

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");
Glina
źródło
7

Opierając się na podejściu Sama Saffrona (i Mike'a Gleasona), oto rozwiązanie, które pozwoli na wiele dzieci i wiele poziomów.

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

Wtedy możesz go odczytać poza funkcją.

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

Funkcję map można następnie wywołać ponownie dla następnego obiektu podrzędnego przy użyciu tego samego obiektu nadrzędnego. Można również zaimplementować podziały w instrukcjach odczytu nadrzędnego lub podrzędnego niezależnie od funkcji map.

Oto dodatkowa metoda rozszerzenia „jeden do 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
źródło
2
Dzięki za to - świetne rozwiązanie. usunięto instrukcję if, aby zamiast wywoływać addChilder bez elementów podrzędnych, funkcja wywołująca mogła obsłużyć wartości null. W ten sposób mogę dodać puste listy, z którymi znacznie łatwiej się pracuje.
Mladen Mihajlovic
1
To fantastyczne rozwiązanie. Miałem problemy z „dynamicznym znajdowaniem”. Można to rozwiązać za pomocą tego contactList = multi.MapChild <Contact, Phone, int> (/ * ten sam kod co powyżej * /
granadaCoder
4

Gdy zdecydowaliśmy się przenieść naszą DataAccessLayer do procedur składowanych, a procedury te często zwracają wiele połączonych wyników (przykład poniżej).

Cóż, moje podejście jest prawie takie samo, ale może trochę wygodniejsze.

Oto jak może wyglądać Twój kod:

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


Rozbijmy to ...

Rozbudowa:

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

Mapper:

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

To wszystko, ale jeśli chcesz zrobić szybki test, oto modele i procedura dla Ciebie:

Modele:

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

SP:

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
Sam Sch
źródło
1
Nie wiem, dlaczego do tej pory nie zwrócono uwagi ani nie komentowano tego podejścia, ale uważam je za bardzo interesujące i logicznie skonstruowane. Dzięki za udostępnienie. Myślę, że można zastosować to podejście do funkcji wycenianych w tabeli, a nawet ciągów SQL - różnią się one jedynie typem polecenia. Tylko niektóre rozszerzenia / przeciążenia i to powinno działać dla wszystkich popularnych typów zapytań.
Grimm
aby upewnić się, że dobrze czytam, wymaga to od użytkownika dokładnego określenia kolejności typów, w jakiej procedura zwróci wyniki, czy to prawda? Jeśli na przykład zamienisz Include <Book> i Include <Course>, to zwróci?
cubesnyc
@cubesnyc, nie pamiętam, czy wyrzuca, ale tak, użytkownik musi znać kolejność
Sam Sch
2

Chciałem podzielić się moim rozwiązaniem tego problemu i sprawdzić, czy ktoś ma jakieś konstruktywne opinie na temat zastosowanego przeze mnie podejścia?

W projekcie, nad którym pracuję, mam kilka wymagań, które muszę najpierw wyjaśnić:

  1. Muszę utrzymywać moje POCO tak czyste, jak to tylko możliwe, ponieważ te klasy będą publicznie udostępniane w opakowaniu API.
  2. Moje POCO są w oddzielnej bibliotece klas z powodu powyższego wymagania
  3. Będzie wiele poziomów hierarchii obiektów, które będą się różnić w zależności od danych (więc nie mogę użyć Generic Type Mapper lub musiałbym napisać ich mnóstwo, aby zaspokoić wszystkie możliwe ewentualności)

Tak więc, to, co zrobiłem, to sprawić, by SQL obsłużył hierarchię drugiego - nego poziomu, zwracając pojedynczy ciąg JSON jako kolumnę w oryginalnym wierszu w następujący sposób ( usunięto inne kolumny / właściwości itp., Aby zilustrować ):

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

Następnie moje POCO są budowane jak poniżej:

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

Gdzie POCO dziedziczy z BaseEntity. (Aby zilustrować, wybrałem dość prostą, jednopoziomową hierarchię, jak pokazano we właściwości „Atrybuty” obiektu klienta).

W mojej warstwie danych mam następującą „klasę danych”, która dziedziczy z POCO Client.

internal class dataClient : Client
{
    public string AttributeJson
    {
        set
        {
            Attributes = value.FromJson<List<ClientAttribute>>();
        }
    }
}

Jak widać powyżej, dzieje się tak, że SQL zwraca kolumnę o nazwie „AttributeJson”, która jest odwzorowana na właściwość AttributeJsonw klasie dataClient. Ma to tylko metodę ustawiającą, która deserializuje kod JSON do Attributeswłaściwości Clientklasy dziedziczonej . Klasa dataClient znajduje internalsię w warstwie dostępu do danych, a ClientProvider(moja fabryka danych) zwraca oryginalny klient POCO do wywołującej aplikacji / biblioteki w następujący sposób:

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

Zauważ, że używam Dapper.Contrib i dodałem nową Get<T>metodę, która zwraca plikIEnumerable<T>

W przypadku tego rozwiązania należy zwrócić uwagę na kilka rzeczy:

  1. Istnieje oczywisty kompromis wydajności z serializacją JSON - porównałem to z 1050 wierszami z 2 List<T>właściwościami podrzędnymi , z których każda zawiera 2 encje na liście, i działa przy 279 ms - co jest akceptowalne dla potrzeb moich projektów - dotyczy to również ZERO optymalizacji po stronie SQL, więc powinienem być w stanie zgolić tam kilka ms.

  2. Oznacza to, że do zbudowania JSON dla każdej wymaganej List<T>właściwości wymagane są dodatkowe zapytania SQL , ale znowu mi to odpowiada, ponieważ znam SQL całkiem dobrze i nie jestem tak biegły w dynamice / odbiciu itp., Więc czuję się, jakbym to zrobił większa kontrola nad rzeczami, bo tak naprawdę rozumiem, co się dzieje pod maską :-)

Może być lepsze rozwiązanie niż to, a jeśli tak, to naprawdę byłbym wdzięczny za wysłuchanie twoich przemyśleń - to jest tylko rozwiązanie, które wymyśliłem, które do tej pory pasuje do moich potrzeb w tym projekcie (chociaż jest to eksperymentalne na etapie wysyłania) ).

Dave Long
źródło
To jest interesujące. Jest jakaś szansa, że ​​możesz udostępnić część SQL?
WhiteRuski