객체 계층을 생성하는 다중 매퍼


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는 각각 두 대의 전화기로 두 개의 연락처 개체를 튀어 나올 것입니다.

저장 프로 시저의 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는 항상 데이터베이스 행당 객체를 반환합니다.

따라서 이것은 많은-> 한 방향에서는 잘 작동하지만 하나-> 많은 다중 맵에서는 잘 작동하지 않습니다.

여기에는 두 가지 문제가 있습니다.

  1. 쿼리와 함께 작동하는 내장 매퍼를 도입하면 중복 데이터를 "삭제"할 수 있습니다. (Contacts. *가 검색어에 중복됩니다.)

  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 2011-06-20

@Jorin, 다른 옵션은 여러 연결을 조정하고 결과를 짜는 것입니다. 조금 까다 롭습니다.
Sam Saffron 2011 년

1
또한 if (childMap.TryGetvalue (..)) 뒤에 else를 추가하여 자식 항목이없는 경우 자식 컬렉션이 기본적으로 NULL 대신 빈 컬렉션으로 초기화되도록합니다. 다음과 같이 : else {addChildren (item, new TChild [] {}); }
Marius

1
@SamSaffron 나는 Dapper를 사랑합니다. 감사합니다. 그래도 질문이 있습니다. 일대다는 SQL 쿼리에서 흔히 발생합니다. 디자인에서 구현자가 사용하기 위해 염두에 둔 것은 무엇입니까? 나는 Dapper 방식으로하고 싶지만 현재 SQL 방식으로 진행 중입니다. One Side가 일반적으로 "드라이버"인 SQL에서이 문제에 대해 어떻게 생각합니까? Dapper에서 Many side가 왜 그렇게됩니까? 요점은 우리가 객체를 얻고 사실 후에 구문 분석을 수행하는 것입니까? 훌륭한 도서관에 감사드립니다.
johnny

2
작업에 적합한 도구를 사용하고 있는지 확인하십시오. 대규모 데이터베이스 성능 요구 사항이 없거나 시스템을 벤치마킹하지 않은 경우 Dapper를 사용하여 몇 시간 또는 며칠을 낭비한 것입니다.
Aluan Haddad

32

참고로-다음을 수행하여 Sam의 대답을 얻었습니다.

먼저 "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
와! 저에게는 이것이 가장 쉬운 해결책이라는 것을 알았습니다. 물론, 일대 다의 경우 (두 개의 테이블 가정) 이중 선택을 사용합니다. 그러나 제 경우에는 일대 다가 있고 이것은 훌륭하게 작동합니다. 이제 많은 중복 데이터를 다시 가져 오지만 제 경우에는이 중복성이 상대적으로 작습니다. 기껏해야 10 행입니다.
code5 2014

이것은 두 가지 수준에서 잘 작동하지만 더 많이 있으면 까다로워집니다.
Samir Aguiar

1
자식 데이터가 없으면 (s, a) 코드가 a = null로 호출되고 Accounts에는 비어있는 대신 null 항목이있는 목록이 포함됩니다. "shop.Accounts.Add (a)"앞에 "if (a! = null)"을 추가해야합니다.
Etienne

12

다중 결과 세트 지원

귀하의 경우에는 다중 결과 집합 쿼리를 사용하는 것이 훨씬 더 좋을 것입니다. 이것은 단순히 두 개의 select 문을 작성해야 함을 의미합니다.

  1. 연락처를 반환하는 사람
  2. 그리고 그들의 전화 번호를 돌려주는

이렇게하면 개체가 고유하고 복제되지 않습니다.


1
다른 답변은 고유 한 방식으로 우아 할 수 있지만 코드가 추론하기 쉽기 때문에이 답변을 좋아하는 경향이 있습니다. 소수의 select 문과 약 30 줄의 foreach / linq 코드를 사용하여 몇 가지 수준의 계층 구조를 구성 할 수 있습니다. 이것은 엄청난 결과 세트로 무너질 수 있지만 다행히도 나는 그 문제가 없습니다 (아직).
Sam Storie 2015

10

다음은 사용하기 쉬운 재사용 가능한 솔루션입니다. Andrews 답변 의 약간의 수정입니다 .

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

Sam Saffron (및 Mike Gleason)의 접근 방식을 기반으로 한 여기에는 여러 어린이와 여러 수준을 허용하는 솔루션이 있습니다.

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
감사합니다-훌륭한 솔루션입니다. 자식이없는 경우 addChilder를 호출하지 않는 대신 호출 함수가 null을 처리 할 수 ​​있도록 if 문을 제거했습니다. 이렇게하면 작업하기 훨씬 쉬운 빈 목록을 추가 할 수 있습니다.
Mladen Mihajlovic

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

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

1
이 접근법이 왜 지금까지 주목이나 의견을받지 못했는지는 모르겠지만 매우 흥미롭고 논리적으로 구조화되어 있습니다. 공유해 주셔서 감사합니다. 이 접근 방식을 테이블 반환 함수 또는 SQL 문자열에도 적용 할 수 있다고 생각합니다. 명령 유형 만 다를뿐입니다. 일부 확장 / 오버로드 만 있으면 모든 일반적인 쿼리 유형에서 작동합니다.
Grimm

이 권한을 읽고 있는지 확인하려면 사용자가 프로 시저가 결과를 반환 할 유형 순서를 정확히 알아야합니다. 맞습니까? 예를 들어 Include <Book>과 Include <Course>를 바꾼 경우 이것은 던질까요?
cubesnyc

@cubesnyc 던지면 기억 나지 않지만 예, 사용자는 주문을 알아야합니다
Sam Sch

2

이 문제에 대한 내 솔루션을 공유하고 내가 사용한 접근 방식에 대한 건설적인 피드백이 있는지 확인하고 싶었습니다.

작업중인 프로젝트에 먼저 설명해야 할 몇 가지 요구 사항이 있습니다.

  1. 이 클래스는 API 래퍼에서 공개적으로 공유되므로 POCO를 가능한 한 깨끗하게 유지해야합니다.
  2. 내 POCO는 위의 요구 사항으로 인해 별도의 클래스 라이브러리에 있습니다.
  3. 데이터에 따라 달라질 수있는 여러 개체 계층 구조 수준이있을 것입니다 (따라서 Generic Type Mapper를 사용할 수 없거나 가능한 모든 상황을 처리하기 위해 많은 것을 작성해야합니다).

그래서 내가 한 일은 다음과 같이 원래 행의 열로 단일 JSON 문자열을 반환하여 2nd-nth Level 계층을 처리하도록 SQL을 얻는 것입니다 (설명하기 위해 다른 열 / 속성 등을 제거했습니다 ).

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은 AttributeJsondataClient 클래스 의 속성 에 매핑 된 "AttributeJson"이라는 열을 반환합니다 . 여기에는 Attributes상속 된 Client클래스 의 속성에 대한 JSON을 deserialises하는 setter 만 있습니다. dataClient 클래스는 internal데이터 액세스 계층에 있으며 ClientProvider(내 데이터 팩토리)는 다음과 같이 호출 앱 / 라이브러리에 원래 클라이언트 POCO를 반환합니다.

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

Dapper.Contrib을 사용하고 있으며 다음 Get<T>을 반환하는 새 메서드를 추가했습니다 .IEnumerable<T>

이 솔루션과 관련하여 몇 가지 유의해야 할 사항이 있습니다.

  1. JSON 직렬화에는 명백한 성능 절충이 있습니다. 2 개의 하위 List<T>속성 이있는 1050 개의 행에 대해 벤치마킹했습니다 . 각각 목록에 2 개의 엔티티가 있고 279ms에서 클럭됩니다. 이는 내 프로젝트 요구 사항에 적합합니다. SQL 측면에서 제로 최적화를 수행하므로 몇 ms를 줄일 수 있습니다.

  2. 각 필수 List<T>속성에 대한 JSON을 빌드하려면 추가 SQL 쿼리가 필요 하지만 다시 말하지만 SQL을 잘 알고 있고 역학 / 반사 등에 능숙하지 않기 때문에이 방식이 저에게 적합합니다. 내가 실제로 무슨 일이 일어나고 있는지 이해함에 따라 사물에 대한 더 많은 통제력 :-)

이보다 더 나은 솔루션이있을 수 있으며 만약 있다면 여러분의 생각을 들어 주셔서 감사하겠습니다. 이것은 지금까지이 프로젝트에 대한 제 요구에 맞는 솔루션입니다 (게시 단계에서 실험적 임에도 불구하고 ).


이것은 흥미 롭다. SQL 부분을 공유 할 수 있습니까?
WhiteRuski
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.