Como mapeio listas de objetos aninhados com o Dapper

127

Atualmente, estou usando o Entity Framework para meu acesso ao banco de dados, mas quero dar uma olhada no Dapper. Eu tenho aulas como esta:

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

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

Assim, um curso pode ser ministrado em vários locais. O Entity Framework faz o mapeamento para mim, para que meu objeto Curso seja preenchido com uma lista de locais. Como eu faria isso com o Dapper, isso é possível ou eu tenho que fazer isso em várias etapas de consulta?

b3n
fonte
Pergunta relacionada: stackoverflow.com/questions/6379155/…
Jeroen K
aqui está a minha solução: stackoverflow.com/a/57395072/8526957
Sam Sch

Respostas:

57

O Dapper não é um ORM completo, ele não lida com a geração mágica de consultas e coisas do tipo.

Para o seu exemplo específico, o seguinte provavelmente funcionaria:

Pegue os cursos:

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

Pegue o mapeamento relevante:

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

Pegue os locais relevantes

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

Mapeie tudo

Deixando isso para o leitor, você cria alguns mapas e percorre os cursos preenchendo os locais.

Ressalva o intruque irá funcionar se você tem menos de 2100 pesquisas (SQL Server), se você tiver mais você provavelmente vai querer alterar a consulta para select * from CourseLocations where CourseId in (select Id from Courses ... )se isso é o caso, você pode muito bem arrancar todos os resultados de uma só vez utilizandoQueryMultiple

Sam Saffron
fonte
Obrigado pelo esclarecimento Sam. Como você descreveu acima, estou apenas executando uma segunda consulta, buscando os Locais e atribuindo-os manualmente ao curso. Eu só queria ter certeza de que não perdi algo que me permitisse fazê-lo com uma consulta.
233
2
Sam, em um aplicativo grande em que as coleções são regularmente expostas em objetos de domínio, como no exemplo, onde você recomendaria que esse código fosse localizado fisicamente ? (Supondo que você queira consumir uma entidade [Course] similarmente totalmente construída a partir de vários lugares diferentes no seu código) No construtor? Em uma fábrica de classe? Em outro lugar?
tbone
177

Como alternativa, você pode usar uma consulta com uma pesquisa:

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;

Veja aqui https://www.tritac.com/blog/dappernet-by-example/

Jeroen K
fonte
9
Isso me salvou uma tonelada de tempo. Uma modificação que eu precisava e que outras pessoas precisam é incluir o argumento splitOn: já que eu não estava usando o "Id" padrão.
Bill Sambrone
1
Para LEFT JOIN, você receberá um item nulo na lista de locais. Remova-os por var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
Não consigo compilar isso, a menos que tenha um ponto-e-vírgula no final da linha 1 e remova a vírgula antes de 'AsQueryable ()'. Gostaria de editar a resposta, mas 62 upvoters antes me parecia pensar que está tudo bem, talvez eu estou faltando alguma coisa ...
bitcoder
1
Para LEFT JOIN: Não precisa fazer outro Foreach nele. Basta verificar antes de adicioná-lo: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Desde que você está usando um dicionário. Isso seria mais rápido se você usasse o QueryMultiple e consultasse o curso e o local separadamente e usasse o mesmo dicionário para atribuir o local ao curso? É basicamente a mesma coisa menos a junção interna, o que significa que o sql não transferirá tantos bytes?
MIKE
43

Não há necessidade de lookupdicionário

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();
tchelidze
fonte
3
Isso é excelente - na minha opinião, essa deve ser a resposta selecionada. As pessoas que fazem isso, no entanto, ficam atentas ao fazer *, pois isso pode afetar o desempenho.
Cr1pto 01/12/2017
2
O único problema é que você duplicará o cabeçalho em cada registro de local. Se houver muitos locais por curso, pode haver uma quantidade significativa de duplicação de dados atravessando a conexão que aumentará a largura de banda, levará mais tempo para analisar / mapear e usar mais memória para ler tudo isso.
Daniel Lorenz
10
Não tenho certeza se isso está funcionando como eu esperava. Eu tenho um objeto pai com 3 objetos relacionados. a consulta que eu uso recebe três linhas de volta. as primeiras colunas que descrevem o pai que são duplicadas para cada linha; a divisão no id identificaria cada filho único. meus resultados são 3 pais duplicados com 3 filhos .... deve ser um pai com 3 filhos.
topwik
2
@topwik está certo. também não funciona como esperado para mim.
Maciej Pszczolinski 4/03/19
3
Na verdade, acabei com 3 pais, 1 filho em cada um com esse código. Não sei por que meu resultado é diferente do @topwik, mas ainda assim, ele não funciona.
Th3morg 15/03/19
29

Eu sei que estou realmente atrasado para isso, mas há outra opção. Você pode usar QueryMultiple aqui. Algo assim:

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();
}
Daniel Lorenz
fonte
3
Uma coisa a notar. Se houver muitos locais / cursos, você deve percorrer os locais uma vez e colocá-los em uma pesquisa de dicionário para ter N log N em vez de N ^ 2. Faz uma grande diferença em conjuntos de dados maiores.
Daniel Lorenz
6

Desculpe estar atrasado para a festa (como sempre). Para mim, é mais fácil usar um Dictionary, como Jeroen K , em termos de desempenho e legibilidade. Além disso, para evitar a multiplicação de cabeçalho entre locais , eu uso Distinct()para remover possíveis dups:

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;
}
Francisco Tena
fonte
4

Algo está faltando. Se você não especificar cada campo Locationsna consulta SQL, o objeto Locationnão poderá ser preenchido. Dê uma olhada:

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;

Usando l.*a consulta, eu tinha a lista de locais, mas sem dados.

Eduardo Pires
fonte
0

Não tenho certeza se alguém precisa, mas tenho uma versão dinâmica sem o Model para codificação rápida e flexível.

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;
Kiichi
fonte