Desenvolvimento

29 ago, 2017

Dapper: relacionamentos um-para-um e um-para-muitos (exemplos em ASP.NET Core)

Publicidade

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.

Referências