Dapper로 중첩 된 개체 목록을 매핑하는 방법


127

현재 DB 액세스를 위해 Entity Framework를 사용하고 있지만 Dapper를 살펴보고 싶습니다. 다음과 같은 수업이 있습니다.

public class Course{
   public string Title{get;set;}
   public IList<Location> Locations {get;set;}
   ...
}

public class Location{
   public string Name {get;set;}
   ...
}

따라서 하나의 코스를 여러 위치에서 가르 칠 수 있습니다. Entity Framework는 저를 위해 매핑을 수행하므로 코스 개체가 위치 목록으로 채워집니다. Dapper로이 문제를 해결하려면 어떻게해야합니까? 가능합니까 아니면 여러 쿼리 단계에서 수행해야합니까?



여기 내 솔루션입니다 : stackoverflow.com/a/57395072/8526957
샘의 SCH

답변:


57

Dapper는 완전한 ORM이 아니며 쿼리의 매직 생성 등을 처리하지 않습니다.

특정 예의 경우 다음이 작동합니다.

과정 파악 :

var courses = cnn.Query<Course>("select * from Courses where Category = 1 Order by CreationDate");

관련 매핑을 가져옵니다.

var mappings = cnn.Query<CourseLocation>(
   "select * from CourseLocations where CourseId in @Ids", 
    new {Ids = courses.Select(c => c.Id).Distinct()});

관련 위치 파악

var locations = cnn.Query<Location>(
   "select * from Locations where Id in @Ids",
   new {Ids = mappings.Select(m => m.LocationId).Distinct()}
);

모든 것을 매핑

이것을 독자에게 맡기고 몇 개의 맵을 만들고 위치로 채워지는 코스를 반복합니다.

주의in 미만의 경우 트릭이 작동 2,100 조회 (SQL 서버)이있는 경우, 더 당신은 아마에 쿼리를 수정하려면 select * from CourseLocations where CourseId in (select Id from Courses ... )사용 가서 그게 당신이뿐만 아니라 하나의 모든 결과를 꺼낼 수있는 경우에 해당QueryMultiple


설명 Sam에 감사드립니다. 위에서 설명한 것처럼 위치를 가져와 수동으로 코스에 할당하는 두 번째 쿼리를 실행하고 있습니다. 나는 단지 내가 하나의 쿼리로 그것을 할 수있게 해주는 무언가를 놓치지 않았는지 확인하고 싶었다.
b3n 2011 년

2
Sam, 예에서와 같이 도메인 개체에 컬렉션이 정기적으로 노출되는 ~ large 응용 프로그램 에서이 코드를 물리적으로 어디에 배치하는 것이 좋 습니까? ( 코드의 여러 다른 위치에서 유사하게 완전히 구성된 [Course] 엔터티를 사용하고 싶다고 가정하면 ) 생성자에서? 클래스 공장에서? 다른 곳?
tbone 2015-08-27

177

또는 조회와 함께 하나의 쿼리를 사용할 수 있습니다.

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course))
            lookup.Add(c.Id, course = c);
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(l); /* Add locations to course */
        return course;
     }).AsQueryable();
var resultList = lookup.Values;

여기를 참조하십시오 https://www.tritac.com/blog/dappernet-by-example/


9
이로 인해 많은 시간이 절약되었습니다. 다른 사람들이 필요할 수있는 한 가지 수정은 기본 "Id"를 사용하지 않았기 때문에 splitOn : 인수를 포함하는 것입니다.
Bill Sambrone

1
LEFT JOIN의 경우 위치 목록에 null 항목이 표시됩니다. var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith

줄 1 끝에 세미콜론이 있고 'AsQueryable ()'앞의 쉼표를 제거하지 않는 한 컴파일 할 수 없습니다. ... 나는 대답을 편집 할 것이지만, 내 앞에 62 upvoters은 좋아, 어쩌면 내가 부족 뭔가를 생각하는 듯
bitcoder

1
LEFT JOIN의 경우 : 다른 Foreach를 수행 할 필요가 없습니다. 추가하기 전에 확인하십시오 : if (l! = null) course.Locations.Add (l).
jpgrassi

1
사전을 사용하고 있기 때문입니다. QueryMultiple을 사용하고 질의 된 코스와 위치를 별도로 사용한 다음 동일한 사전을 사용하여 코스에 위치를 할당하면 더 빠를까요? 본질적으로 내부 조인을 빼면 SQL이 많은 바이트를 전송하지 않음을 의미합니까?
MIKE

43

lookup사전 필요 없음

var coursesWithLocations = 
    conn.Query<Course, Location, Course>(@"
        SELECT c.*, l.*
        FROM Course c
        INNER JOIN Location l ON c.LocationId = l.Id                    
        ", (course, location) => {
            course.Locations = course.Locations ?? new List<Location>();
            course.Locations.Add(location); 
            return course;
        }).AsQueryable();

3
이것은 훌륭합니다. 제 생각에는 이것이 선택된 대답이어야합니다. 하지만 이렇게하는 사람들은 성능에 영향을 줄 수 있으므로 *를 조심해야합니다.
cr1pto

2
유일한 문제는 모든 위치 레코드에서 헤더를 복제한다는 것입니다. 코스 당 위치가 많은 경우 대역폭을 증가시키고 파싱 / 맵핑하는 데 더 많은 시간이 소요되고 모든 것을 읽는 데 더 많은 메모리를 사용하는 상당한 양의 데이터 중복이 유선을 통해 진행될 수 있습니다.
Daniel Lorenz

10
예상대로 작동하는지 잘 모르겠습니다. 3 개의 관련 개체가있는 1 개의 상위 개체가 있습니다. 내가 사용하는 쿼리는 세 행을 다시 가져옵니다. 각 행에 대해 중복되는 상위를 설명하는 첫 번째 열; ID 분할은 각각의 고유 한 자식을 식별합니다. 내 결과는 3 명의 자녀가있는 3 명의 중복 부모입니다 .... 3 명의 자녀가있는 한 부모 여야합니다.
topwik

2
@topwik이 맞습니다. 나에게도 예상대로 작동하지 않습니다.
Maciej Pszczolinski

3
나는 실제로이 코드를 가진 3 명의 부모, 각각 1 명의 자녀로 끝났습니다. 내 결과가 @topwik와 다른 이유를 잘 모르겠지만 여전히 작동하지 않습니다.
th3morg

29

나는 이것에 정말로 늦었다는 것을 알고 있지만 다른 옵션이 있습니다. 여기에서 QueryMultiple을 사용할 수 있습니다. 이 같은:

var results = cnn.QueryMultiple(@"
    SELECT * 
      FROM Courses 
     WHERE Category = 1 
  ORDER BY CreationDate
          ; 
    SELECT A.*
          ,B.CourseId 
      FROM Locations A 
INNER JOIN CourseLocations B 
        ON A.LocationId = B.LocationId 
INNER JOIN Course C 
        ON B.CourseId = B.CourseId 
       AND C.Category = 1
");

var courses = results.Read<Course>();
var locations = results.Read<Location>(); //(Location will have that extra CourseId on it for the next part)
foreach (var course in courses) {
   course.Locations = locations.Where(a => a.CourseId == course.CourseId).ToList();
}

3
주목해야 할 한 가지. 위치 / 코스가 많은 경우 위치를 한 번 반복하고 사전 조회에 넣어 N ^ 2 속도 대신 N log N을 가져야합니다. 더 큰 데이터 세트에서 큰 차이를 만듭니다.
Daniel Lorenz

6

파티에 늦어서 죄송합니다 (언제나 그렇듯이). 나를 위해, 그것은을 사용하는 것이 더 쉽습니다 Dictionary, 제론 K했던 것처럼 성능과 가독성의 측면에서. 또한 위치 간 헤더 곱셈을 피하기 위해 Distinct()잠재적 중복을 제거 하는 데 사용 합니다.

string query = @"SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id";
using (SqlConnection conn = DB.getConnection())
{
    conn.Open();
    var courseDictionary = new Dictionary<Guid, Course>();
    var list = conn.Query<Course, Location, Course>(
        query,
        (course, location) =>
        {
            if (!courseDictionary.TryGetValue(course.Id, out Course courseEntry))
            {
                courseEntry = course;
                courseEntry.Locations = courseEntry.Locations ?? new List<Location>();
                courseDictionary.Add(courseEntry.Id, courseEntry);
            }

            courseEntry.Locations.Add(location);
            return courseEntry;
        },
        splitOn: "Id")
    .Distinct()
    .ToList();

    return list;
}

4

무언가가 빠졌어. LocationsSQL 쿼리에서 각 필드를 지정하지 않으면 개체 Location를 채울 수 없습니다. 구경하다:

var lookup = new Dictionary<int, Course>()
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.Name, l.otherField, l.secondField
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course)) {
            lookup.Add(c.Id, course = c);
        }
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(a);
        return course;
     },
     ).AsQueryable();
var resultList = lookup.Values;

l.*쿼리에서 사용하면 위치 목록이 있지만 데이터는 없습니다.


0

누구에게 필요한지 확실하지 않지만 빠르고 유연한 코딩을 위해 Model없이 동적 버전이 있습니다.

var lookup = new Dictionary<int, dynamic>();
conn.Query<dynamic, dynamic, dynamic>(@"
    SELECT A.*, B.*
    FROM Client A
    INNER JOIN Instance B ON A.ClientID = B.ClientID                
    ", (A, B) => {
        // If dict has no key, allocate new obj
        // with another level of array
        if (!lookup.ContainsKey(A.ClientID)) {
            lookup[A.ClientID] = new {
                ClientID = A.ClientID,
                ClientName = A.Name,                                        
                Instances = new List<dynamic>()
            };
        }

        // Add each instance                                
        lookup[A.ClientID].Instances.Add(new {
            InstanceName = B.Name,
            BaseURL = B.BaseURL,
            WebAppPath = B.WebAppPath
        });

        return lookup[A.ClientID];
    }, splitOn: "ClientID,InstanceID").AsQueryable();

var resultList = lookup.Values;
return resultList;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.