Dapper.Net에서 일대 다 쿼리를 어떻게 작성합니까?


80

일대 다 관계를 프로젝트하기 위해이 코드를 작성했지만 작동하지 않습니다.

using (var connection = new SqlConnection(connectionString))
{
   connection.Open();

   IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
                        (@"Select Stores.Id as StoreId, Stores.Name, 
                                  Employees.Id as EmployeeId, Employees.FirstName,
                                  Employees.LastName, Employees.StoreId 
                           from Store Stores 
                           INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
                        (a, s) => { a.Employees = s; return a; }, 
                        splitOn: "EmployeeId");

   foreach (var store in stores)
   {
       Console.WriteLine(store.Name);
   }
}

누가 실수를 발견 할 수 있습니까?

편집하다:

다음은 내 엔티티입니다.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public IList<Store> Stores { get; set; }

    public Product()
    {
        Stores = new List<Store>();
    }
}

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public IEnumerable<Employee> Employees { get; set; }

    public Store()
    {
        Products = new List<Product>();
        Employees = new List<Employee>();
    }
}

편집하다:

쿼리를 다음과 같이 변경합니다.

IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
        (@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
           Employees.FirstName,Employees.LastName,Employees.StoreId 
           from Store Stores INNER JOIN Employee Employees 
           ON Stores.Id = Employees.StoreId",
         (a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");

그리고 나는 예외를 제거합니다! 그러나 직원은 전혀 매핑되지 않습니다. 나는 여전히 IEnumerable<Employee>첫 번째 쿼리에서 어떤 문제가 있었는지 잘 모르겠습니다 .


1
엔티티는 어떻게 생겼습니까?
기드온

2
어떻게 작동하지 않습니까? 예외가 발생합니까? 예상치 못한 결과?
driis

1
오류는 의미가 없으므로 게시하지 않은 이유입니다. "{"값은 null 일 수 없습니다. \ r \ n 매개 변수 이름 : con "}"이 표시됩니다. SqlMapper에서 오류를 발생시키는 줄은 다음과 같습니다. "il.Emit (OpCodes.Newobj, type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));"
TCM

답변:


162

이 게시물에서는 고도로 정규화 된 SQL 데이터베이스 를 쿼리 하고 결과를 고도로 중첩 된 C # POCO 객체 세트에 매핑하는 방법을 보여줍니다 .

성분 :

  • 8 줄의 C #.
  • 일부 조인을 사용하는 합리적으로 간단한 SQL.
  • 두 개의 멋진 라이브러리.

이 문제를 해결할 수있는 통찰력 MicroORMmapping the result back to the POCO Entities. 따라서 두 개의 개별 라이브러리를 사용합니다.

기본적으로 Dapper 를 사용하여 데이터베이스를 쿼리 한 다음 Slapper.Automapper 를 사용 하여 결과를 POCO 에 직접 매핑합니다.

장점

  • 단순성 . 8 줄 미만의 코드입니다. 이해, 디버그 및 변경하기가 훨씬 더 쉽습니다.
  • 적은 코드 . 몇 줄의 코드는 모두 Slapper입니다. Automapper 는 복잡한 중첩 POCO (즉, POCO가를 포함 List<MyClass1>하는 POCO 포함)가 있더라도 사용자가 던지는 모든 것을 처리해야합니다 List<MySubClass2>.
  • 속도 . 이 두 라이브러리는 모두 수작업으로 조정 된 ADO.NET 쿼리만큼 빠르게 실행할 수 있도록 엄청난 양의 최적화 및 캐싱 기능을 갖추고 있습니다.
  • 우려의 분리 . MicroORM을 다른 것으로 변경할 수 있으며 매핑은 여전히 ​​작동하며 그 반대도 마찬가지입니다.
  • 유연성 . Slapper.Automapper 는 임의로 중첩 된 계층 구조를 처리하며 두 단계의 중첩 수준으로 제한되지 않습니다. 우리는 쉽게 빠르게 변경할 수 있으며 모든 것이 여전히 작동합니다.
  • 디버깅 . 먼저 SQL 쿼리가 제대로 작동하는지 확인한 다음 SQL 쿼리 결과가 대상 POCO 엔터티에 다시 올바르게 매핑되었는지 확인할 수 있습니다.
  • SQL의 개발 용이성 . inner joins플랫 결과를 반환하기 위해 플랫 쿼리를 만드는 것이 클라이언트 측에서 스티칭을 사용하여 여러 select 문을 만드는 것보다 훨씬 쉽다는 것을 알았습니다.
  • SQL에서 최적화 된 쿼리 . 고도로 정규화 된 데이터베이스에서 플랫 쿼리를 생성하면 SQL 엔진이 전체에 고급 최적화를 적용 할 수 있습니다. 이는 많은 작은 개별 쿼리가 구성되고 실행되는 경우 일반적으로 가능하지 않습니다.
  • 신뢰 . Dapper는 StackOverflow의 백엔드이며 Randy Burden은 약간의 슈퍼 스타입니다. 더 말할 필요가 있습니까?
  • 개발 속도. 여러 수준의 중첩을 사용하여 매우 복잡한 쿼리를 수행 할 수 있었고 개발 시간이 상당히 짧았습니다.
  • 더 적은 버그. 나는 한 번 썼고 방금 효과가 있었고이 기술은 이제 FTSE 회사에 힘을 실어주고 있습니다. 코드가 너무 적어 예기치 않은 동작이 없었습니다.

단점

  • 1,000,000 개 이상의 행이 반환되었습니다. 100,000 개 미만의 행을 반환 할 때 잘 작동합니다. 그러나 1,000,000 개 이상의 행을 다시 가져 오는 경우, 우리와 SQL 서버 간의 트래픽을 줄이기 위해이를 사용하여 평면화해서는 안됩니다 inner join(중복을 가져옴). 대신 여러 select문을 사용 하고 모든 것을 다시 연결해야합니다. 클라이언트 측 (이 페이지의 다른 답변 참조).
  • 이 기술은 쿼리 지향적 입니다. 이 기술을 데이터베이스에 쓰는 데 사용하지는 않았지만 StackOverflow 자체가 Dapper를 DAL (Data Access Layer)로 사용하기 때문에 Dapper는 추가 작업을 통해이 작업을 수행 할 수있는 것 이상이라고 확신합니다.

성능 시험

내 테스트에서 Slapper.Automapper 는 Dapper가 반환 한 결과에 약간의 오버 헤드를 추가했습니다. 이는 여전히 Entity Framework보다 10 배 더 빠르며 조합은 여전히 ​​SQL + C #이 할 수있는 이론적 최대 속도에 가깝습니다 .

대부분의 실제 경우 대부분의 오버 헤드는 C # 측에서 결과를 매핑하는 것이 아니라 최적화되지 않은 SQL 쿼리에 있습니다.

성능 테스트 결과

총 반복 횟수 : 1000

  • Dapper by itself: 쿼리 당 1.889 밀리 초, 3 lines of code to return the dynamic.
  • Dapper + Slapper.Automapper: 쿼리 당 2.463 밀리 초, 추가 3 lines of code for the query + mapping from dynamic to POCO Entities.

작동 예

이 예에서 우리는의 목록을 가지고 Contacts있고 각각 Contact은 하나 이상의 phone numbers.

POCO 법인

public class TestContact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<TestPhone> TestPhones { get; set; }
}

public class TestPhone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
}

SQL 테이블 TestContact

여기에 이미지 설명 입력

SQL 테이블 TestPhone

이 테이블에는 테이블을 참조하는 외래 키 ContactID가 있습니다 TestContact( List<TestPhone>위의 POCO에 해당 ).

여기에 이미지 설명 입력

플랫 결과를 생성하는 SQL

SQL 쿼리에서는 JOIN필요한 모든 데이터를 비정규 화 된 형식 으로 가져 오는 데 필요한 만큼 많은 문을 사용 합니다 . 예, 이것은 출력에 중복을 생성 할 수 있지만 이러한 중복은 Slapper.Automapper 를 사용 하여이 쿼리의 결과를 POCO 객체 맵에 자동으로 매핑 할 때 자동으로 제거됩니다 .

USE [MyDatabase];
    SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId

여기에 이미지 설명 입력

C # 코드

const string sql = @"SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";

string connectionString = // -- Insert SQL connection string here.

using (var conn = new SqlConnection(connectionString))
{
    conn.Open();    
    // Can set default database here with conn.ChangeDatabase(...)
    {
        // Step 1: Use Dapper to return the  flat result as a Dynamic.
        dynamic test = conn.Query<dynamic>(sql);

        // Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
        // - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
        //   let it know the primary key for each POCO.
        // - Must also use underscore notation ("_") to name parameters in the SQL query;
        //   see Slapper.Automapper docs.
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });

        var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();      

        foreach (var c in testContact)
        {                               
            foreach (var p in c.TestPhones)
            {
                Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);   
            }
        }
    }
}

산출

여기에 이미지 설명 입력

POCO 엔티티 계층

Visual Studio에서 보면, 우리는 우리가이 즉, Slapper.Automapper 제대로 우리의 POCO 엔터티를 채워 것을 볼 수 있습니다 List<TestContact>, 각각 TestContactA가 들어 List<TestPhone>.

여기에 이미지 설명 입력

메모

Dapper와 Slapper.Automapper는 속도를 위해 모든 것을 내부적으로 캐시합니다. 메모리 문제가 발생할 가능성이 거의없는 경우 두 가지 모두에 대한 캐시를 가끔 지워야합니다.

결과를 POCO 엔터티에 매핑하는 방법에 대한 단서를 Slapper.Automapper에 제공하기 위해 밑줄 ( _) 표기법 을 사용하여 다시 오는 열의 이름을 지정해야합니다 .

각 POCO 엔티티의 기본 키에 대한 Slapper.Automapper 단서를 제공해야합니다 (라인 참조 Slapper.AutoMapper.Configuration.AddIdentifiers). 이를 Attributes위해 POCO에서 사용할 수도 있습니다 . 이 단계를 건너 뛰면 Slapper.Automapper가 매핑을 올바르게 수행하는 방법을 알지 못하므로 이론적으로 잘못 될 수 있습니다.

2015-06-14 업데이트

이 기술을 정규화 된 40 개 이상의 테이블이있는 대규모 프로덕션 데이터베이스에 성공적으로 적용했습니다. 그것은 16 이상과 고급 SQL 쿼리를 매핑 완벽하게 작동 inner join하고 left join적절한 POCO 계층에 (중첩의 4 개 수준). 쿼리는 ADO.NET에서 직접 코딩하는 것만 큼 빠릅니다 (일반적으로 쿼리의 경우 52 밀리 초, 플랫 결과에서 POCO 계층 구조로 매핑하는 경우 50 밀리 초). 이것은 실제로 혁신적인 것은 아니지만 속도와 사용 편의성면에서 Entity Framework를 능가합니다. 특히 우리가하는 모든 작업이 쿼리를 실행하는 경우에는 더욱 그렇습니다.

업데이트 2016-02-19

코드는 9 개월 동안 프로덕션에서 완벽하게 실행되었습니다. 의 최신 버전 Slapper.Automapper에는 SQL 쿼리에서 반환되는 null과 관련된 문제를 해결하기 위해 적용한 모든 변경 사항이 있습니다.

업데이트 2017-02-20

코드는 21 개월 동안 프로덕션에서 완벽하게 실행되었으며 FTSE 250 회사에서 수백 명의 사용자의 지속적인 쿼리를 처리했습니다.

Slapper.Automapper.csv 파일을 POCO 목록에 직접 매핑하는데도 좋습니다. .csv 파일을 IDictionary 목록으로 읽어 들인 다음 대상 POCO 목록에 직접 매핑합니다. 유일한 트릭은 속성을 추가하고 int Id {get; set}모든 행에 대해 고유한지 확인해야한다는 것입니다 (그렇지 않으면 자동 매퍼가 행을 구분할 수 없습니다).

업데이트 2019-01-29

더 많은 코드 주석을 추가하기위한 사소한 업데이트.

참조 : https://github.com/SlapperAutoMapper/Slapper.AutoMapper


1
나는 모든 SQL에서 테이블 이름 접두사 규칙을 좋아하지 않지만 Dapper의 "splitOn"과 같은 것을 지원하지 않습니까?
tbone

3
이 테이블 이름 규칙은 Slapper.Automapper에 필요합니다. 예, Dapper는 POCO에 대한 직접 매핑을 지원하지만 코드가 매우 깨끗하고 유지 관리가 가능하기 때문에 Slapper.Automapper를 사용하는 것을 선호합니다.
증권 결제 유예 금

2
모든 열의 별칭을 지정할 필요가 없다면 Slapper를 사용할 것이라고 생각합니다. 대신 귀하의 예에서 다음과 같이 말할 수 있습니다. 모든 별칭을 지정하는 것보다 쉬울까요?
tbone 2015-06-11

1
나는 슬 래퍼의 모습이 정말 마음에 들어요. 연락처가 없을 때 왼쪽 조인을 시도했는지 궁금한가요? 그것을 다루는 좋은 방법이 있습니까?
사랑하지 않음

1
@tbone splitOn이 개체에서이 요소가 슬 래퍼는이 같은 경로를 사용하는 이유입니다 속한 위치에 대한 모든 정보를 포함 나던
사랑하지

20

가능한 한 간단하게 유지하고 싶었습니다.

public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
    var sql = @"
    select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, 
        d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, 
        d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
    from 
        t_data d
    where d.cd_data = @DataId order by id_data asc;

    select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
    from 
        t_data d
        inner join T_data_image di on d.id_data = di.cd_data
        inner join T_image i on di.cd_image = i.id_image 
    where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";

    var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
    var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
    var images = mapper.Read<ForumMessageImage>().ToList();

    foreach(var imageGroup in images.GroupBy(g => g.DataId))
    {
        messages[imageGroup.Key].Images = imageGroup.ToList();
    }

    return messages.Values.ToList();
}

나는 여전히 데이터베이스에 대한 하나의 호출을 수행하고 이제 하나 대신 두 개의 쿼리를 실행하는 동안 두 번째 쿼리는 덜 최적의 LEFT 조인 대신 INNER 조인을 사용합니다.


5
나는이 접근 방식을 좋아합니다. Pure dapper 및 IMHO 더 이해하기 쉬운 매핑.
Avner

1
이것은 키 선택기 하나와 자식 선택기 하나 인 두 개의 albmdas를 사용하는 확장 메서드에 쉽게 넣을 수있을 것 같습니다. 유사 .Join(하지만 평면화 된 결과 대신 객체 그래프를 생성합니다.
AaronLS

8

대신 Func를 사용하여 부모 키를 선택하는 Andrew의 대답을 약간 수정했습니다 GetHashCode.

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

사용 예

conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)

이 솔루션에서 주목해야 할 한 가지는 부모 클래스가 자식 속성을 인스턴스화한다는 것입니다. class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
클레이

1
이 솔루션은 훌륭하고 우리에게 효과적입니다. 반환 된 자식 행이없는 경우 null을 확인하기 위해 children.add와 함께 수표를 추가해야했습니다.
tlbignerd

7

이 답변 에 따르면 Dapper.Net에는 일대 다 매핑 지원이 없습니다. 쿼리는 항상 데이터베이스 행당 하나의 개체를 반환합니다. 하지만 대체 솔루션이 포함되어 있습니다.


죄송하지만 쿼리에서 어떻게 사용하는지 이해가 안 되나요? 조인없이 데이터베이스를 두 번 쿼리하려고합니다 (예에서는 하드 코딩 된 1 사용). 이 예제에는 반환되는 주 엔터티가 1 개만 있으며 하위 엔터티가 포함됩니다. 제 경우에는 프로젝트 참여를 원합니다 (내부적으로 목록이 포함 된 목록). 언급하신 링크로 어떻게해야합니까? 라인이 말하는 링크에서 : (contact, phones) => { contact.Phones = phones; } 연락처 ID가 연락처의 연락처 ID와 일치하는 전화에 대한 필터를 작성해야합니다. 이것은 매우 비효율적입니다.
TCM

@Anthony Mike의 대답을 살펴보십시오. 그는 두 개의 결과 집합이 포함 된 단일 쿼리를 실행하고 나중에이를 Map 메서드와 결합합니다. 물론 귀하의 경우에는 값을 하드 코딩 할 필요가 없습니다. 몇 시간 후에 예제를 작성해 보겠습니다.
Damir Arh

1
좋아 마침내 작동하게되었습니다. 감사! 이것이 단일 조인을 사용하여 수행 할 수있는 것보다 데이터베이스 쿼리 성능에 2 배의 영향을 미칠지 모릅니다.
TCM

2
또한 3 개의 테이블이 있었다면 어떤 변경을해야하는지 이해가되지 않습니다. : p
TCM

1
이건 완전히 짜증나 .. 도대체 왜 조인을 피하는가?
GorillaApe 2012 년

2

다음은 조잡한 해결 방법입니다.

    public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
    {
        var cache = new Dictionary<int, TOne>();
        cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
                                            {
                                                if (!cache.ContainsKey(one.GetHashCode()))
                                                    cache.Add(one.GetHashCode(), one);

                                                var localOne = cache[one.GetHashCode()];
                                                var list = property(localOne);
                                                list.Add(many);
                                                return localOne;
                                            }, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
        return cache.Values;
    }

결코 가장 효율적인 방법은 아니지만 시작하고 실행할 수 있습니다. 기회가 생기면 이것을 최적화하려고 노력할 것입니다.

다음과 같이 사용하십시오.

conn.Query<Product, Store>("sql here", prod => prod.Stores);

객체가 GetHashCode다음과 같이 구현해야 함을 명심하십시오 .

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }

11
캐시 구현에 결함이 있습니다. 해시 코드는 고유하지 않습니다. 두 개체가 동일한 해시 코드를 가질 수 있습니다. 이로 인해 다른 개체에 속한 항목으로 개체 목록이 채워질 수 있습니다.
stmax

2

다음은 또 다른 방법입니다.

Order (하나)-OrderDetail (다수)

using (var connection = new SqlCeConnection(connectionString))
{           
    var orderDictionary = new Dictionary<int, Order>();

    var list = connection.Query<Order, OrderDetail, Order>(
        sql,
        (order, orderDetail) =>
        {
            Order orderEntry;

            if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
            {
                orderEntry = order;
                orderEntry.OrderDetails = new List<OrderDetail>();
                orderDictionary.Add(orderEntry.OrderID, orderEntry);
            }

            orderEntry.OrderDetails.Add(orderDetail);
            return orderEntry;
        },
        splitOn: "OrderDetailID")
    .Distinct()
    .ToList();
}

출처 : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many

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