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

문서화 된 게시물 / 사용자 예제 와 매우 흡사 한 것처럼 보이지만 약간 다르며 나를 위해 작동하지 않는 것 같기 때문에 나는 이것을 조금 가지고 놀았 습니다.

다음과 같은 단순화 된 설정을 가정합니다 (연락처에 여러 전화 번호가 있음).

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

내가 뭔가 잘못하고 있습니까? 포스트 / 소유자 예제와 똑같이 보입니다. 단, 제가 자식이 아닌 부모에서 자식으로가는 것을 제외하면 말입니다.

미리 감사드립니다



답변

당신은 잘못한 것이 없으며 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;
}

이것은 약간 까다 롭고 복잡하기 때문에주의해야합니다. 나는 이것을 핵심에 포함시키는쪽으로 기울지 않고있다.


답변

참고로-다음을 수행하여 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;
    }
}


답변

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


답변

다중 결과 세트 지원

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

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

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


답변

다음은 사용하기 쉬운 재사용 가능한 솔루션입니다. 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");


답변

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


답변

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