Qué es un ORM - introducción Dapper

En este post trataremos sobre dapper, un micro orm que nos permite mapear de nuestra base de datos a C#

 

1 - Qué es Dapper?

Primero debemos entender que es dapper, el cual es un micro ORM para .NET, de hecho es el micro ORM más popular dentro de  la comunidad de .NET y está creado y mantenido por el equipo de Stackoverflow.

 

2 - Qué es un micro ORM?

“Micro ORM” viene de las siglas “object relational mapper”, esto quiere decir que un ORM es el responsable de mapear de la base de datos a nuestros objetos en C#.

 

Depende el ORM que estés utilizando realizará más acciones o menos, un micro ORM se centra únicamente en el trabajo de hacer el mapeo entre la base de datos y los objetos. 

 

2.1 - ORM vs Micro ORM

Dapper no es el único micro ORM, con casi total seguridad hayas escuchado hablar de Entity Framework, este es otro ORM, pero en este caso no es micro. 

 

Un ORM tradicional (Entity framework) no realiza únicamente el mapeo entre la base de datos y los objetos, sino que además nos genera el SQL que va a ser ejecutado por detrás. 

Mientras que un micro ORM se centra solo en el mapeo, y es el desarrollador el que debe escribir el SQL.

Nota: no hay uno mejor o peor, son dos formas de programar, completamente válidas.

 

2.2 - Características de un micro ORM

Ligero: Los micro ORM tienen la característica de que son muy ligeros al no tener un diseñador (como si lo tiene entity framework), o no tiene un XML para ser configurado. Está creado para que los desarrolladores se centren en programar.

Velocidad: Utilizando un micro ORM la consulta SQL tiene que ser escrita a mano, puede darse el caso de que sea mucho más rápida que una escrita de forma automática por entity framework.

Fácil de utilizar: Como está indicado anteriormente, Dapper está enfocado para los desarrolladores, por lo que está pensado para ser “plug and play”. Además la cantidad de métodos que debemos aprender es mínima y sencilla.  

 

3 - Características principales de Dapper

Hasta ahora he hablado de las características de los (micro) ORM, pero qué es lo que hace especial a Dapper.

 

Consulta y mapeo: Dapper en concreto se centra en hacer un mapeo rápido y preciso de nuestros objetos, además los parámetros de las queries están parametrizados, con lo que evitaremos inyección SQL. 

Rendimiento: Dapper es el rey de los ORM en términos de rendimiento, para conseguir esto, extiende la interfaz IDbConnection, lo que implica que es un poco más cercano “al core” del lenguaje, y nos da beneficios de rendimiento. Dapper tiene en su página de GitHub una lista con el rendimiento comparado con otros ORM.

API muy sencilla: El objetivo de dapper es hacer un par de funcionalidades y hacerlas todas muy bien. La api, nos provee de tres tipos de métodos.

  • Métodos que mapean tipos concretos.
  • Métodos que mapean tipos dinámicos.
  • Métodos para ejecutar comandos, como por ejemplo insert o delete; 

Cualquier base de datos: otro beneficio muy grande es que funciona con cualquier tipo de base de datos. Yo personalmente suelo trabajar con MySQL, pero PostgreSQL o SQL Server funcionan perfectamente. 

 

4 - Uso de Dapper en nuestro código

Utilizaremos para este ejemplo el código de la web de C#, el cual está disponible en su apartado del blog.

 

Lo primero que tenemos que hacer es ir a nuget y descargar la última versión de dapper:

dapper nugget

Depués lo que tenemos que hacer es migrar nuestro código escrito utilizando nuestra conexión de forma “pura” como vimos en el post de conectarse a una base de datos mysql por la forma de realizar esta acción con dapper.  

 

4.1 - Añadir la conexión al Contenedor de dependencias

Este paso no es completamente necesario, pero si nos facilita mucho la vida a la hora de programar. 

 

Lo primero que debemos hacer es ir a nuestro código y añadir al contenedor de dependencias  la clase DbConnection, pero que no va a hacer referencia a ninguna interfaz, si no que construiremos la clase aquí mismo

services
    .AddScoped<DbConnection>(x => new MySqlConnection("Server=127.0.0.1;Port=3306;Database=webpersonal;Uid=webpersonaluser;password=webpersonalpass;"))

Nota: para mostrar el ejemplo la conexión entera está “hardcoded” pero en circunstancias normales deberíamos tenerla en secrets/vault, o en el “peor” escenario, en los ficheros de configuración. 

services
     .AddScoped<DbConnection>(x => new MySqlConnection(configuration.GetValue<string>("conexionMysql")))

 

Una vez tenemos DbConnection en nuestro DI pasamos a inyectarlo a cada uno de los repositorios como parámetro.

public abstract class BaseRepository<T>
    where T : class
{

    protected readonly DbConnection _conexion;

    public BaseRepository(DbConnection conexion)
    {
        _conexion = conexion;
    }
}

 

4.2 - Por qué utilizar DBConnection?

El motivo principal es porque DBconnection se traga cualquier tipo de conexión, en este caso utilizamos MySQL pero es posible que quisiéramos cambiar a PostgreSQL en ese escenario únicamente deberíamos cambiar nuestra definición en el inyector de dependencias en vez de cada uno de los repositorios.

 

Por supuesto debemos cambiar donde utilizamos MySQLConnection por nuestra conexión inyectada para nuestro proyecto actual.

 

4. 3 - Ejemplo Query de forma “pura”. 

Para recoger un usuario de la forma vista anteriormente (que he denominado “pura”)  tenemos algo como lo siguiente:

public async Task<T?> GetByUserId(int userId)
{
    using (MySqlConnection conexion = new MySqlConnection(ConnectionString))
    {
        conexion.Open();
        MySqlCommand cmd = new MySqlCommand();
        cmd.Connection = conexion;
        cmd.CommandText = $"select * from {TableName} where UserId = ?userId";
        cmd.Parameters.Add("?userId", MySqlDbType.Int32).Value = userId;
        T? result = null;
        DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection);

        while (await reader.ReadAsync())
        {
            result = Create(reader);
        }
        return result;
    }
}

public abstract T? Create(DbDataReader reader);

Como hemos tenemos los siguientes puntos:

  • Conexión
  • Query
  • Parámetros /consulta
  • Mapeo a nuestro objeto,

en este caso, devolvemos <T?> donde T es un tipo que puede ser PersonalProfileEntity o el propio usuario. 

Este mapeo lo tenemos que hacer de forma manual

public override PersonalProfileEntity? Create(DbDataReader reader)
{
    return PersonalProfileEntity.Create(
        Convert.ToInt32(reader[nameof(PersonalProfileEntity.UserId)]),
        Convert.ToInt32(reader[nameof(PersonalProfileEntity.Id)]),
        reader[nameof(PersonalProfileEntity.FirstName)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.LastName)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.Description)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.Phone)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.Email)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.Website)].ToString() ?? "",
        reader[nameof(PersonalProfileEntity.GitHub)].ToString() ?? ""
        );
}

 

Lo cual como podemos imaginar lleva bastante trabajo.

 

Cuando llamamos a la URL para coger el usuario nos devuelve el valor correcto:

Url: https://localhost:44363/api/PerfilPersonal/ivanabad
Respuesta:
{
    "valor": {
        "userId": 1,
        "id": 1,
        "userName": "ivanabad",
        "firstName": "ivan",
        "lastName": "abad",
        "description": "test desdcp",
        "phone": "elf",
        "email": "ivan@netmentor.es",
        "website": "https://www.netmentor.es",
        "gitHub": "/ElectNEwt",
        "interests": [],
        "skills": []
    },
    "errores": [],
    "success": true
}

 

5 - Preparar el código para dapper

Ahora que hemos visto la “dificultad” de no utilizar ningún tipo de ORM vamos a ver los beneficios que nos trae utilizar uno cuando llegamos al código.

 

El objetivo final es el mismo, vamos a recoger un PersonalProfileEntity de la base de datos.

Para replicar la misma consulta utilizando dapper únicamente debemos utilizar nuestra conexión dentro de un bloque using

 

Y en nuestras entidades crear un constructor, o hacer los setters públicos para que la librería de dapper pueda acceder a ellos. 

Personalmente recomiendo un constructor con un modificador de acceso protected. 

 

En caso de crear un constructor, debemos crear los parámetros de entrada en el mismo orden que están en la base de datos, y respetando si son mayúsculas o minúsculas.  En caso de que tus columnas no sean llamadas del mismo modo deberás utilizar alias de SQL en la propia consulta.  

 

Por ejemplo, si en la base de datos tenemos, id, nombre, email, el constructor deberá tener ese orden, si por ejemplo el constructor es id, email, Nombre, este no funcionara (nos saltará un error).

public class PersonalProfileEntity
{
    public readonly int UserId;
    public readonly int? Id;
    public readonly string FirstName;
    public readonly string LastName;
    public readonly string Description;
    public readonly string Phone;
    public readonly string Email;
    public readonly string Website;
    public readonly string GitHub;

    protected PersonalProfileEntity(int? id, int userid, string firstname, string description, string phone, string email,
        string lastname, string website, string github)
    {
        UserId = userid;
        Id = id;
        FirstName = firstname;
        LastName = lastname;
        Description = description;
        Phone = phone;
        Email = email;
        Website = website;
        GitHub = github;
    }

    public static PersonalProfileEntity Create(int userId, int? id, string firstName, string lastName, string description,
        string phone, string email, string website, string gitHub)
        => new PersonalProfileEntity(id, userId, firstName, description, phone, email, lastName, website, gitHub);
}

 

6 - Posibles acciones con Dapper

Dapper nos proporciona una serie de extension methods los cuales nos permiten realizar diferentes acciones contra la base de datos.

 

Todas las acciones (métodos) tienen su versión normal y su versión con genercis la cual acepta <T> por ejemplo consultar el primer ítem es tanto QueryFirst(..) como QueryFirst<T>(...) la diferencia es que la que incluye métodos genéricos nos hará el mapeo automáticamente. 

 

Lo mismo pasa con sus versiones asíncronas y síncronas, disponemos de QueryFirstAsync<T> y de QueryFirst<T>.

 

Para nuestro ejemplo veremos las versiones de tipos genéricos y asíncronas.

 

6.1 - Consultas con dapper

Para realizar consultas disponemos de dos métodos principales

 

A - Consultar un único ítem con dapper

Cuando tenemos una relación 1 - 1 solemos consultar un único ítem, ya sea recogiendo el primero de una forma manual de acceso resultado[0] o haciendo top 1 en SQL. 

Dapper nos da una solución a este problema y nos provee del método QueryFirstAsync() o QueryFristAsync<T>.

 

Para convertir el código que hemos visto en el apartado 4.3 a consultas utilizando dapper es muy sencillo, ya que no volveremos a necesitar el mapeo, y lucirá tal que así:

public async Task<T?> GetByUserId(int userId)
{
    using(var con = _conexion)
    {
        con.Open();

        return await con.QueryFirstAsync<T?>($"select * from {TableName} where UserId = @userId", new
        {
            userId = userId
        });
    }
}

nota: también podemos utilizar QuerySingleAsync<T>.

 

B - Consultar Listas con dapper

Para consultar listas, o más de un resultado la funcionalidad es similar a la anterior, disponemos de un método llamado QueryAsync<T> el cual devuelve IEnumerable<T>.

public async Task<List<T>> GetListByUserId(int userId)
{
    using (var con = _conexion)
    {
        con.Open();
        return (await con.QueryAsync<T>($"select * from {TableName} where UserId = @userId", new
        {
            userId = userId
        })).ToList();
    }
}

 

6.2 - Insertar datos con dapper

Para insertar datos, el código es algo más complejo, pero no mucho más, y esto es debido a que debemos construir nuestra query de SQL en el propio código. 

Debemos indicar los campos a insertar así como sus valores, pero no el valor a insertar, sino la propiedad del objeto que queremos insertar, por ejemplo para el campo “nombre” indicaremos @Nombre haciendo referencia a la propia propiedad. 

 

Para ejecutar nuestro insert con dapper utilizaremos el método QueryAsync<T>.

public async Task<PersonalProfileEntity> Insertar(PersonalProfileEntity perfilPersonal)
{
    string sql = $"insert into {TableName} ({nameof(PersonalProfileEntity.UserId)}, {nameof(PersonalProfileEntity.FirstName)}, " +
            $"{nameof(PersonalProfileEntity.LastName)}, {nameof(PersonalProfileEntity.Description)}, {nameof(PersonalProfileEntity.Phone)}," +
            $"{nameof(PersonalProfileEntity.Email)}, {nameof(PersonalProfileEntity.Website)}, {nameof(PersonalProfileEntity.GitHub)}) " +
            $"values (@{nameof(PersonalProfileEntity.UserId)}, @{nameof(PersonalProfileEntity.FirstName)}, @{nameof(PersonalProfileEntity.LastName)}," +
            $"@{nameof(PersonalProfileEntity.Description)}, @{nameof(PersonalProfileEntity.Phone)}, @{nameof(PersonalProfileEntity.Email)}," +
            $"@{nameof(PersonalProfileEntity.Website)}, @{nameof(PersonalProfileEntity.GitHub)} );" +
            $"Select CAST(SCOPE_IDENTITY() as int)";
    using(var con = _conexion)
    {
        con.Open();
        var newId = (await con.QueryAsync<int>(sql, perfilPersonal)).First();
        return PersonalProfileEntity.UpdateId(perfilPersonal, newId);
    }
}

La parte de Select CAST(SCOPE_IDENTITY() as int) es para que nos devuelva el ID que acaba de insertar.

 

6.3 - Actualizar datos con dapper

Para actualizar utilizaremos un proceso similar al anterior, ya que también debemos escribir la query completa con los datos que queremos actualizar.

 

Pero esta vez utilizaremos el método ExecuteAsync el resultado de la operación es el número de filas afectadas. 

public async Task<PersonalProfileEntity> Update(PersonalProfileEntity perfilPersonal)
{
    string sql = $"Update {TableName} " +
        $"set {nameof(PersonalProfileEntity.FirstName)} = @{nameof(PersonalProfileEntity.FirstName)}, " +
            $"{nameof(PersonalProfileEntity.LastName)} = @{nameof(PersonalProfileEntity.LastName)}, " +
            $"{nameof(PersonalProfileEntity.Description)} = @{nameof(PersonalProfileEntity.Description)}, " +
            $"{nameof(PersonalProfileEntity.Phone)} = @{nameof(PersonalProfileEntity.Phone)}," +
            $"{nameof(PersonalProfileEntity.Email)} = @{nameof(PersonalProfileEntity.Email)}, " +
            $"{nameof(PersonalProfileEntity.Website)} = @{nameof(PersonalProfileEntity.Website)}, " +
            $"{nameof(PersonalProfileEntity.GitHub)} = @{nameof(PersonalProfileEntity.GitHub)}" +
            $"Where {nameof(PersonalProfileEntity.Id)} = @{nameof(PersonalProfileEntity.UserId)}";
            
    using (var con = _conexion)
    {
        con.Open();
        int filasAfectadas = await con.ExecuteAsync(sql, perfilPersonal);
        return perfilPersonal;
    }
}

 

6.4 - Borrar datos con dapper

Para eliminar datos utilizando dapper, el proceso es similar a actualizar, la diferencia es que en este caso debemos escribir una consulta SQL para borrar dichos datos. También utilizaremos el método ExecuteAsync.

public async Task<int> Delete(int id)
{
    string sql = $"delete from {TableName} Where {nameof(PersonalProfileEntity.Id)} = @id";

    using (var con = _conexion)
    {
        con.Open();
        await con.ExecuteAsync(sql, new { id = id });
        return id;
    }
}

 

 

Conclusión

  • En este post hemos visto que es un ORM y un Micro ORM.
  • Las diferencias entre un ORM y un Micro ORM.
  • Los beneficios de utilizar Un (Micro) ORM frente a conexiones “puras”.
  • Qué es Dapper y sus beneficios.
  • Introducción al uso de Dapper en nuestro código. 
Comparte