객체 계층 구조를 만드는 다중 매퍼

dapper multi-mapping

문제

나는 문서화 된 게시물 / 사용자 예제 처럼 많이 느껴졌지만 조금씩 다르며 나에게 효과적이지 않은 것처럼 보였으므로 조금만 놀아 봤다.

다음 간단한 설정을 가정합니다 (연락처에 전화 번호가 여러 개인 경우).

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 객체로 Contact를 반환하는 무언가로 끝내고 싶습니다. 그런 식으로 2 개의 전화가있는 2 개의 연락처가있는 경우 SQL은 4 개의 총 행이 설정된 결과 집합으로 SQL을 조인합니다. 그러면 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 [전화]'형식으로 캐스팅 할 수 없습니다."

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가 설계된 방식이 아니다. 모든 Query API는 항상 데이터베이스 행당 오브젝트를 리턴합니다.

따라서 이것은 여러 방향에서 잘 작동하지만, 하나의 맵에서 많은 맵을 사용하는 것은 좋지 않습니다.

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

  1. 쿼리와 함께 작동하는 내장 매퍼 (mapper)를 도입하면 중복 데이터를 "폐기"해야합니다. (연락처. *는 쿼리에서 중복됩니다.)

  2. 우리가 one -> many pair로 작업하도록 설계한다면, 일종의 identity map이 필요할 것입니다. 복잡성이 추가됩니다.


예를 들어 제한된 수의 레코드를 가져와야하는 경우에 효율적으로 사용할 수있는이 쿼리를 생각해보십시오.이 작업을 100 만 건으로 밀어 넣는다면 더 까다로워 질 수 있습니다. 스트리밍해야하고 모든 것을 메모리에로드 할 수 없기 때문입니다.

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와 매퍼 (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;
}

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


인기 답변

참고 - 다음과 같이 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;
    }
}



아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.
아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.