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?



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


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/


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

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

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

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.


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;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.