Embora mais limitado quando comparado às soluções mais sofisticadas de acesso a dados relacionais (como o Entity Framework e o NHibernate), o micro-ORM Dapper ainda assim desfruta de grande popularidade entre desenvolvedores .NET. Um dos principais motivos para isto se deve à alta performance que o mesmo propicia, sobretudo na execução de queries mais complexas.
Já abordei o uso do Dapper em um artigo anterior, no qual apresentei diversos exemplos em .NET Full e .NET Core: Dapper: exemplos de utilização em ASP.NET Core e .NET Full.
Neste novo artigo, demonstrarei como implementar relacionamentos um-para-um e um-para-muitos com este micro-ORM, considerando, para isto, a execução de uma única consulta/query englobando mais de uma tabela.
Observação: embora os exemplos descritos a seguir façam uso do ASP.NET Core 1.1 e do Visual Studio 2017, tais implementações também são válidas para projetos baseados no .NET Full (como aplicações MVC 5 ou Web API 2, por exemplo).
Implementando relacionamentos do tipo um-para-um
A implementação de um relacionamento um-para-um é suportada pelo Dapper sem maiores complicações. Do ponto de vista prático, faremos uso apenas deste framework e não será necessário nenhum recurso adicional (diferentemente de situações envolvendo um-para-muitos, assunto da próxima seção).
Tomemos agora, por exemplo, uma classe chamada Estado, cujo código está detalhado na listagem a seguir:
- A propriedade DadosRegiao (tipo Regiao) contém dados da área geográfica da qual um estado faz parte;
- Podemos concluir, a partir, disto que a tabela estados está vinculada a uma estrutura do mesmo tipo, esta última englobando dados de regiões.
namespace ExemploASPNETCoreMultiMapping { public class Estado { public string SiglaEstado { get; set; } public string NomeEstado { get; set; } public string NomeCapital { get; set; } public Regiao DadosRegiao { get; set; } } }
A definição do tipo Regiao está na listagem seguinte:
namespace ExemploASPNETCoreMultiMapping { public class Regiao { public int IdRegiao { get; set; } public string NomeRegiao { get; set; } } }
Supondo agora uma query que relaciona dados das duas tabelas (estados e regiões):
SELECT * FROM dbo.Estados E INNER JOIN dbo.Regioes R ON R.IdRegiao = E.IdRegiao ORDER BY E.NomeEstado
E cujo resultado pode ser visualizado na próxima imagem:
Como podemos executar esta única consulta no banco de dados, devolvendo como resultado instâncias do tipo Estado com suas propriedades DadosRegiao devidamente preenchidas? O Dapper conta com um mecanismo de multi mapping que viabiliza a codificação deste tipo de necessidade.
Na listagem a seguir, podemos observar o uso desta funcionalidade na implementação do tipo EstadosController:
- O método Query conta com uma sobrecarga na qual devem ser especificadas as diferentes classes empregadas na produção do resultado. O último tipo informado corresponde à classe dos objetos retornados (Estado, neste caso);
- O parâmetro map faz uso de uma expressão lambda, a fim de mapear os diferentes tipos de objetos gerados e devolver via instrução return a instância principal (tipo Estado). Os parâmetros informados aqui seguem a mesma ordem do método Query, com a referência indicada pelo parâmetro regiao sendo associada à propriedade DadosRegiao do objeto estado;
- No parâmetro splitOn estão definidos os campos-chave dos objetos a serem gerados. Esta informação serve de base para que o Dapper efetue a separação dos dados em suas instâncias equivalentes
using System.Collections.Generic; using System.Data.SqlClient; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Dapper; namespace ExemploASPNETCoreMultiMapping.Controllers { [Route("api/[controller]")] public class EstadosController : Controller { [HttpGet] public IEnumerable<Estado> Get( [FromServices]IConfiguration config) { using (SqlConnection conexao = new SqlConnection( config.GetConnectionString("ExemplosDapper"))) { return conexao.Query<Estado, Regiao, Estado>( "SELECT * " + "FROM dbo.Estados E " + "INNER JOIN dbo.Regioes R ON R.IdRegiao = E.IdRegiao " + "ORDER BY E.NomeEstado", map: (estado, regiao) => { estado.DadosRegiao = regiao; return estado; }, splitOn: "SiglaEstado,IdRegiao"); } } } }
As próximas imagens trazem o resultado da execução deste Controller:
Implementando relacionamentos do tipo um-para-muitos
A implementação de relacionamentos um-para-muitos também é possível com o Dapper. Para o exemplo desta seção, faremos uso de implementação diferente da classe Regiao, na qual é possível observar a existência de uma coleção chamada Estados (esta última baseada no tipo Estado e contendo dados das diversas unidades federais que integram uma área geográfica):
using System.Collections.Generic; namespace APIRegioes { public class Regiao { public int IdRegiao { get; set; } public string NomeRegiao { get; set; } public List<Estado> Estados { get; set; } } public class Estado { public string SiglaEstado { get; set; } public string NomeEstado { get; set; } public string NomeCapital { get; set; } } }
Será empregado aqui o package Slapper.AutoMapper (ainda em versão beta para .NET Core), além do Dapper e do provider ADO.NET para SQL Server:
O Slapper.Automapper permite o mapeamento de dados dinâmicos para objetos, incluindo a capacidade de preencher coleções empregadas no relacionamento entre diferentes tipos.
A query, a seguir, lista as regiões e os estados associados a estas divisões geográficas:
SELECT R.IdRegiao, R.NomeRegiao, E.SiglaEstado, E.NomeEstado, E.NomeCapital FROM dbo.Regioes R INNER JOIN dbo.Estados E ON E.IdRegiao = R.IdRegiao ORDER BY R.NomeRegiao, E.NomeEstado
O resultado da execução desta consulta pode ser observado na próxima imagem:
Na próxima listagem está a implementação da classe RegioesController:
- O retorno do método Query (uma extensão disponibilizada pelo Dapper) será uma coleção de objetos dinâmicos. Na instrução SQL informada como parâmetro a esta operação é possível notar que as colunas que se referem a propriedades do tipo Estado tiverem seu alias alterado, de forma que a identificação de cada campo contenha o nome da coleção a ser mapeada (Estados), o caracter underline (“_”) e a propriedade correspondente na classe-filha (Estado, no caso).
- Serão acessadas, então, as classes AutoMapper e Config do framework Slapper, a fim de invocar a operação AddIdentifier. Este método receberá como parâmetro o tipo a ser mapeado e o nome da propriedade correspondente à chave primária. Esta ação deverá acontecer para cada classe envolvida no relacionamento;
- Por fim, a chamada ao método MapDynamic da classe AutoMapper converterá o resultado para uma lista de instâncias do tipo Regiao. A propriedade Estados de cada objeto gerado conterá, por sua vez, diversas referências da classe Estado.
using System.Collections.Generic; using System.Linq; using System.Data.SqlClient; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Dapper; using Slapper; namespace APIRegioes.Controllers { [Route("api/[controller]")] public class RegioesController : Controller { [HttpGet] public IEnumerable<Regiao> Get( [FromServices]IConfiguration config) { using (SqlConnection conexao = new SqlConnection( config.GetConnectionString("ExemplosDapper"))) { var dados = conexao.Query<dynamic>( "SELECT R.IdRegiao, " + "R.NomeRegiao, " + "E.SiglaEstado AS Estados_SiglaEstado, " + "E.NomeEstado AS Estados_NomeEstado, " + "E.NomeCapital AS Estados_NomeCapital " + "FROM dbo.Regioes R " + "INNER JOIN dbo.Estados E " + "ON E.IdRegiao = R.IdRegiao " + "ORDER BY R.NomeRegiao, E.NomeEstado"); AutoMapper.Configuration.AddIdentifier( typeof(Regiao), "IdRegiao"); AutoMapper.Configuration.AddIdentifier( typeof(Estado), "SiglaEstado"); List<Regiao> regioes = (AutoMapper.MapDynamic<Regiao>(dados) as IEnumerable<Regiao>).ToList(); return regioes; } } } }
Nas próximas imagens é possível visualizar o resultado da execução desta aplicação:
Os fontes do projeto detalhado nesta seção também foram disponibilizados no GitHub. Se tiver interesse em consultar e/ou baixar tal conteúd,o acesse este link.