Multi-Mapper zum Erstellen einer Objekthierarchie

82

Ich habe ein bisschen damit herumgespielt, weil es sich anscheinend sehr nach dem Beispiel für dokumentierte Beiträge / Benutzer anfühlt , aber es ist etwas anders und funktioniert bei mir nicht.

Angenommen, die folgende vereinfachte Einrichtung (ein Kontakt hat mehrere Telefonnummern):

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

Ich würde gerne etwas haben, das einen Kontakt mit mehreren Telefonobjekten zurückgibt. Auf diese Weise würde mein SQL, wenn ich 2 Kontakte mit jeweils 2 Telefonen hätte, einen Join dieser als Ergebnismenge mit insgesamt 4 Zeilen zurückgeben. Dann würde Dapper 2 Kontaktobjekte mit jeweils zwei Telefonen herausspringen lassen.

Hier ist die SQL in der gespeicherten Prozedur:

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

Ich habe es versucht, aber am Ende hatte ich 4 Tupel (was in Ordnung ist, aber nicht das, was ich mir erhofft hatte ... es bedeutet nur, dass ich das Ergebnis noch normalisieren muss):

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

und wenn ich eine andere Methode versuche (siehe unten), erhalte ich die Ausnahme "Objekt vom Typ 'System.Int32' kann nicht in 'System.Collections.Generic.IEnumerable`1 [Telefon]' umgewandelt werden".

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

Mache ich nur etwas falsch Es scheint genau wie im Beispiel für Beiträge / Eigentümer, nur dass ich vom Elternteil zum Kind gehe und nicht vom Kind zum Elternteil.

Danke im Voraus

Jorin
quelle

Antworten:

69

Sie machen nichts falsch, es ist einfach nicht die Art und Weise, wie die API entworfen wurde. Alle QueryAPIs geben immer ein Objekt pro Datenbankzeile zurück.

Dies funktioniert also gut für viele -> eine Richtung, aber weniger gut für die eine -> viele Multi-Map.

Hier gibt es zwei Probleme:

  1. Wenn wir einen integrierten Mapper einführen, der mit Ihrer Abfrage funktioniert, wird erwartet, dass wir doppelte Daten "verwerfen". (Kontakte. * Ist in Ihrer Anfrage doppelt vorhanden.)

  2. Wenn wir es so gestalten, dass es mit einem Paar von eins -> vielen funktioniert, benötigen wir eine Art Identitätskarte. Das erhöht die Komplexität.


Nehmen wir zum Beispiel diese Abfrage, die effizient ist, wenn Sie nur eine begrenzte Anzahl von Datensätzen abrufen müssen. Wenn Sie diese auf eine Million übertragen, wird es schwieriger, weil Sie streamen müssen und nicht alles in den Speicher laden können:

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

Was Sie tun können, ist das GridReaderzu erweitern , um die Neuzuordnung zu ermöglichen:

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

Angenommen, Sie erweitern Ihren GridReader und mit einem Mapper:

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

Da dies etwas knifflig und komplex ist, mit Einschränkungen. Ich neige nicht dazu, dies in den Kern aufzunehmen.

Sam Safran
quelle
Sehr cool. Dieses Ding hat ziemlich viel Kraft ... Ich denke, es gewöhnt sich nur daran, wie man es benutzt. Ich werde die Nutzlast meiner Abfragen untersuchen und feststellen, wie groß die Ergebnismengen sind, und prüfen, ob wir es uns leisten können, mehrere Abfragen zu haben und sie zusammen zuzuordnen.
Jorin
@Jorin, Ihre andere Option wäre, mehrere Verbindungen zu orchestrieren und die Ergebnisse zu weben. Es ist etwas schwieriger.
Sam Saffron
1
Ich würde auch ein else nach dem if (childMap.TryGetvalue (..)) hinzufügen, damit die untergeordnete Sammlung standardmäßig mit einer leeren Sammlung anstelle von NULL initialisiert wird, wenn keine untergeordneten Elemente vorhanden sind. So: else {addChildren (item, new TChild [] {}); }
Marius
1
@ SamSaffron Ich liebe Dapper. Vielen Dank. Ich habe allerdings eine Frage. Eins-zu-Viele kommt häufig bei SQL-Abfragen vor. Was hatten Sie beim Entwurf für den Implementierer im Sinn? Ich möchte es auf die Dapper-Art machen, aber ich bin im Moment auf der SQL-Art. Wie denke ich darüber nach, wenn ich aus SQL komme, wo die eine Seite normalerweise der "Treiber" ist? Warum ist die Viele-Seite so in Dapper? Ist es der Punkt, an dem wir das Objekt erhalten und nachträglich analysieren? Danke für die tolle Bibliothek.
Johnny
2
Stellen Sie sicher, dass Sie das richtige Werkzeug für den Job verwenden. Wenn Sie keine massiven Anforderungen an die Datenbankleistung haben oder Ihr System nicht bewertet haben, haben Sie mit Dapper Stunden oder vielleicht Tage Ihres Lebens verschwendet.
Aluan Haddad
32

Zu Ihrer Information: Ich habe Sams Antwort folgendermaßen erhalten:

Zuerst habe ich eine Klassendatei namens "Extensions.cs" hinzugefügt. Ich musste das Schlüsselwort "this" an zwei Stellen in "reader" ändern:

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

Zweitens habe ich die folgende Methode hinzugefügt und den letzten Parameter geändert:

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
quelle
24

Schauen Sie sich https://www.tritac.com/blog/dappernet-by-example/ an. Sie könnten so etwas tun:

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;

Ich habe dies aus den dapper.net-Tests erhalten: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

Jeroen K.
quelle
2
Beeindruckend! Für mich war dies die einfachste Lösung. Zugegeben, für eine -> viele (unter der Annahme von zwei Tabellen) würde ich mich für die doppelte Auswahl entscheiden. In meinem Fall habe ich jedoch eine Eins-> Eins-> Viele und das funktioniert großartig. Jetzt bringt es viele redundante Daten zurück, aber für meinen Fall ist diese Redundanz relativ gering - bestenfalls 10 Zeilen.
Code5
Dies funktioniert gut für zwei Ebenen, aber es wird schwierig, wenn Sie mehr haben.
Samir Aguiar
1
Wenn keine untergeordneten Daten vorhanden sind, wird der Code (s, a) mit a = null aufgerufen, und Accounts enthalten eine Liste mit einem Null-Eintrag, anstatt leer zu sein. Sie müssen "if (a! = Null)" vor "shop.Accounts.Add (a)" hinzufügen
Etienne Charland
12

Unterstützung für mehrere Ergebnismengen

In Ihrem Fall wäre es viel besser (und auch einfacher), eine Abfrage mit mehreren Ergebnismengen zu haben. Dies bedeutet einfach, dass Sie zwei select-Anweisungen schreiben sollten:

  1. Eine, die Kontakte zurückgibt
  2. Und eine, die ihre Telefonnummern zurückgibt

Auf diese Weise wären Ihre Objekte einzigartig und würden nicht dupliziert.

Robert Koritnik
quelle
1
Während die anderen Antworten auf ihre eigene Weise elegant sein mögen, mag ich diese eher, weil der Code leichter zu überlegen ist. Ich kann mit ein paar ausgewählten Anweisungen und etwa 30 Zeilen foreach / linq-Code eine Hierarchie erstellen, die einige Ebenen tief ist. Dies könnte mit massiven Ergebnismengen zusammenbrechen, aber zum Glück habe ich dieses Problem (noch) nicht.
Sam Storie
10

Hier ist eine wiederverwendbare Lösung, die ziemlich einfach zu bedienen ist. Es ist eine leichte Modifikation von Andrews Antwort .

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

Anwendungsbeispiel

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

Basierend auf dem Ansatz von Sam Saffron (und Mike Gleason) ist hier eine Lösung, die mehrere Kinder und mehrere Ebenen ermöglicht.

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

Dann können Sie es außerhalb der Funktion lesen lassen.

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

Die Zuordnungsfunktion kann dann für dasselbe nächste untergeordnete Objekt unter Verwendung desselben übergeordneten Objekts erneut aufgerufen werden. Sie können auch Teilungen für die übergeordneten oder untergeordneten Leseanweisungen unabhängig von der Zuordnungsfunktion implementieren .

Hier ist eine zusätzliche Erweiterungsmethode "Single to 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
quelle
2
Danke dafür - tolle Lösung. Die if-Anweisung wurde entfernt, sodass die aufrufende Funktion die Nullen nicht verarbeiten kann, anstatt addChilder für keine untergeordneten Elemente aufzurufen. Auf diese Weise kann ich leere Listen hinzufügen, mit denen ich viel einfacher arbeiten kann.
Mladen Mihajlovic
1
Dies ist eine fantastische Lösung. Ich hatte einige Probleme mit "dynamischem Finden". Das kann mit dieser contactList = multi.MapChild <Kontakt, Telefon, int> (/ * der gleiche Code wie oben hier * /
granadaCoder
4

Sobald wir uns entschieden haben, unseren DataAccessLayer auf gespeicherte Prozeduren zu verschieben, geben diese Prozeduren häufig mehrere verknüpfte Ergebnisse zurück (Beispiel unten).

Nun, mein Ansatz ist fast der gleiche, aber vielleicht ein bisschen komfortabler.

So könnte Ihr Code aussehen:

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


Lass es uns zusammenfassen ...

Erweiterung:

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

Das ist alles, aber wenn Sie einen Schnelltest durchführen möchten, finden Sie hier Modelle und Verfahren für Sie:

Modelle:

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
quelle
1
Ich weiß nicht, warum dieser Ansatz bisher keine Aufmerksamkeit oder Kommentare erhalten hat, aber ich finde ihn sehr interessant und logisch strukturiert. Danke für das Teilen. Ich denke, Sie können diesen Ansatz auch auf Funktionen mit Tabellenwerten oder sogar auf SQL-Zeichenfolgen anwenden - sie unterscheiden sich lediglich im Befehlstyp. Nur einige Erweiterungen / Überladungen und dies sollte für alle gängigen Abfragetypen funktionieren.
Grimm
Um sicherzustellen, dass ich dieses Recht lese, muss der Benutzer genau wissen, in welcher Typreihenfolge die Prozedur Ergebnisse zurückgibt. Ist das richtig? Wenn Sie beispielsweise Include <Book> und Include <Course> vertauscht hätten, würde dies auslösen?
cubesnyc
@ Cubesnyc Ich erinnere mich nicht, ob es wirft, aber ja, Benutzer muss die Reihenfolge kennen
Sam Sch
2

Ich wollte meine Lösung für dieses Problem teilen und sehen, ob jemand konstruktives Feedback zu dem von mir verwendeten Ansatz hat.

Ich habe einige Anforderungen in dem Projekt, an dem ich arbeite, die ich zuerst erklären muss:

  1. Ich muss meine POCOs so sauber wie möglich halten, da diese Klassen in einem API-Wrapper öffentlich freigegeben werden.
  2. Meine POCOs befinden sich aufgrund der oben genannten Anforderung in einer separaten Klassenbibliothek
  3. Es wird mehrere Objekthierarchieebenen geben, die je nach Daten variieren (daher kann ich keinen generischen Typ-Mapper verwenden oder ich müsste Tonnen davon schreiben, um alle möglichen Eventualitäten zu berücksichtigen).

Was ich also getan habe, ist, SQL dazu zu bringen, die Erben der 2. - n. Ebene zu handhaben, indem eine einzelne JSON-Zeichenfolge als Spalte in der ursprünglichen Zeile wie folgt zurückgegeben wird ( die anderen Spalten / Eigenschaften usw. wurden zur Veranschaulichung entfernt ):

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

Dann werden meine POCOs wie folgt aufgebaut:

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

Wo die POCOs von BaseEntity erben. (Zur Veranschaulichung habe ich eine ziemlich einfache, einstufige Hierarchie gewählt, wie die Eigenschaft "Attribute" des Clientobjekts zeigt.)

Ich habe dann in meiner Datenschicht die folgende "Datenklasse", die vom POCO erbt Client.

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

Wie Sie oben sehen können, gibt SQL eine Spalte mit dem Namen "AttributeJson" zurück, die der Eigenschaft AttributeJsonin der dataClient-Klasse zugeordnet ist. Dies hat nur einen Setter, der den JSON an die AttributesEigenschaft der geerbten ClientKlasse deserialisiert . Die dataClient-Klasse befindet sich internalin der Datenzugriffsschicht und die ClientProvider(meine Datenfactory ) gibt den ursprünglichen Client-POCO wie folgt an die aufrufende App / Bibliothek zurück:

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

Beachten Sie, dass ich Dapper.Contrib verwende und eine neue Get<T>Methode hinzugefügt habe, die eine zurückgibtIEnumerable<T>

Bei dieser Lösung sind einige Dinge zu beachten:

  1. Es gibt einen offensichtlichen Kompromiss zwischen der Leistung und der JSON-Serialisierung. Ich habe dies mit 1050 Zeilen mit 2 Untereigenschaften List<T>verglichen, von denen jede 2 Entitäten in der Liste enthält. Die Taktrate beträgt 279 ms. Dies ist für meine Projektanforderungen akzeptabel NULL-Optimierung auf der SQL-Seite, damit ich mich dort ein paar ms rasieren kann.

  2. Es bedeutet, dass zusätzliche SQL-Abfragen erforderlich sind, um den JSON für jede erforderliche List<T>Eigenschaft aufzubauen , aber auch dies passt zu mir, da ich SQL ziemlich gut kenne und die Dynamik / Reflexion usw. nicht so fließend beherrsche Mehr Kontrolle über die Dinge, da ich tatsächlich verstehe, was unter der Haube passiert :-)

Es gibt möglicherweise eine bessere Lösung als diese, und wenn es eine gibt, würde ich mich sehr freuen, Ihre Gedanken zu hören - dies ist genau die Lösung, die ich bisher gefunden habe und die meinen Anforderungen für dieses Projekt entspricht (obwohl dies zum Zeitpunkt der Veröffentlichung experimentell ist ).

Dave Long
quelle
Das ist interessant. Gibt es eine Chance, dass Sie den SQL-Teil teilen können?
WhiteRuski