DapperExtensions: agrega "insertar ... actualizar en clave duplicada"

c# dapper dapper-extensions orm

Pregunta

Ahora estoy usando Dapper + Dapper.Extensions. Y sí, es fácil e increíble. Pero me enfrenté a un problema: Dapper.Extensions solo tiene el comando Insertar y no InsertUpdateOnDUplicateKey . Quiero agregar dicho método, pero no veo una buena manera de hacerlo:

  1. Quiero hacer que este método sea genérico como Insertar
  2. No puedo obtener una lista de propiedades en caché para un tipo particular porque no quiero usar la reflexión directamente para construir sql en bruto

Posible forma de bifurcarlo en github, pero quiero hacerlo solo en mi proyecto. ¿Alguien sabe cómo extenderlo? Entiendo que esta característica ("insertar ... actualizar en clave duplicada") solo se admite en MySQL. Pero no puedo encontrar puntos de extensión en DapperExtensions para agregar esta funcionalidad fuera.
Actualización: este es mi tenedor https://github.com/MaximTkachenko/Dapper-Extensions/commits/master

Respuesta aceptada

En realidad, cerré mi solicitud de extracción y eliminé mi tenedor porque:

  1. Veo algunas solicitudes de extracción abiertas creadas en 2014
  2. Encontré una forma de "inyectar" mi código en Dapper.Extensions.

Recuerdo mi problema: quiero crear consultas más genéricas para Dapper.Extensions. Significa que necesito tener acceso a caché de mapeo para entidades, SqlGenerator, etc. Así que aquí está mi camino. Quiero agregar la capacidad de hacer INSERTAR .. ACTUALIZAR EN DUPLICATE KEY para MySQL. Creé un método de extensión para ISqlGenerator

   public static class SqlGeneratorExt
    {
        public static string InsertUpdateOnDuplicateKey(this ISqlGenerator generator, IClassMapper classMap)
        {
            var columns = classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly || p.KeyType == KeyType.Identity));
            if (!columns.Any())
            {
                throw new ArgumentException("No columns were mapped.");
            }

            var columnNames = columns.Select(p => generator.GetColumnName(classMap, p, false));
            var parameters = columns.Select(p => generator.Configuration.Dialect.ParameterPrefix + p.Name);
            var valuesSetters = columns.Select(p => string.Format("{0}=VALUES({1})", generator.GetColumnName(classMap, p, false), p.Name));

            string sql = string.Format("INSERT INTO {0} ({1}) VALUES ({2}) ON DUPLICATE KEY UPDATE {3}",
                                       generator.GetTableName(classMap),
                                       columnNames.AppendStrings(),
                                       parameters.AppendStrings(),
                                       valuesSetters.AppendStrings());

            return sql;
        }
    }

Un método de extensión más para IDapperImplementor

public static class DapperImplementorExt
{
    public static void InsertUpdateOnDuplicateKey<T>(this IDapperImplementor implementor, IDbConnection connection, IEnumerable<T> entities, int? commandTimeout = null) where T : class
    {
        IClassMapper classMap = implementor.SqlGenerator.Configuration.GetMap<T>();
        var properties = classMap.Properties.Where(p => p.KeyType != KeyType.NotAKey);
        string emptyGuidString = Guid.Empty.ToString();

        foreach (var e in entities)
        {
            foreach (var column in properties)
            {
                if (column.KeyType == KeyType.Guid)
                {
                    object value = column.PropertyInfo.GetValue(e, null);
                    string stringValue = value.ToString();
                    if (!string.IsNullOrEmpty(stringValue) && stringValue != emptyGuidString)
                    {
                        continue;
                    }

                    Guid comb = implementor.SqlGenerator.Configuration.GetNextGuid();
                    column.PropertyInfo.SetValue(e, comb, null);
                }
            }
        }

        string sql = implementor.SqlGenerator.InsertUpdateOnDuplicateKey(classMap);

        connection.Execute(sql, entities, null, commandTimeout, CommandType.Text);
    }
}

Ahora puedo crear una nueva clase derivada de la clase de base de datos para usar mi propio sql

public class Db : Database
{
    private readonly IDapperImplementor _dapperIml;

    public Db(IDbConnection connection, ISqlGenerator sqlGenerator) : base(connection, sqlGenerator)
    {
        _dapperIml = new DapperImplementor(sqlGenerator);
    }

    public void InsertUpdateOnDuplicateKey<T>(IEnumerable<T> entities, int? commandTimeout) where T : class
    {
        _dapperIml.InsertUpdateOnDuplicateKey(Connection, entities, commandTimeout);
    }
}

Sí, es necesario crear otra instancia de DapperImplementor porque la instancia de DapperImplementor de la clase base es privada :(. Así que ahora puedo usar mi clase Db para llamar a mis propias consultas SQL genéricas y consultas nativas de Dapper.Extension. Ejemplos de uso de clase de base de datos en lugar de Las extensiones de IDbConnection se pueden encontrar aquí .


Respuesta popular

Esta pieza de código me ha ayudado enormemente en proyectos relacionados con MySQL, definitivamente te debo una.

Hago mucho desarrollo relacionado con la base de datos tanto en MySQL como en MS SQL. También trato de compartir la mayor cantidad de código posible entre mis proyectos.

MS SQL no tiene equivalente directo para "ON DUPLICATE KEY UPDATE", por lo que antes no podía usar esta extensión cuando trabajaba con MS SQL.

Al migrar una aplicación web (que se basa en gran medida en este ajuste de Dapper.Extensions) de MySQL a MS SQL, finalmente decidí hacer algo al respecto.

Este código utiliza el enfoque "IF EXISTS => UPDATE ELSE INSERT" que básicamente hace lo mismo que "ON DUPLICATE KEY UPDATE" en MySQL.

Tenga en cuenta: el fragmento asume que se está ocupando de las transacciones fuera de este método. Alternativamente, puede agregar "COMIENZO DE TRAN" al comienzo y "COMPROMISO" al final de la cadena sql generada.

public static class SqlGeneratorExt
{
    public static string InsertUpdateOnDuplicateKey(this ISqlGenerator generator, IClassMapper classMap, bool hasIdentityKeyWithValue = false)
    {
        var columns = classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly || (p.KeyType == KeyType.Identity && !hasIdentityKeyWithValue))).ToList();
        var keys = columns.Where(c => c.KeyType != KeyType.NotAKey).Select(p => $"{generator.GetColumnName(classMap, p, false)}=@{p.Name}");
        var nonkeycolumns = classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly) && p.KeyType == KeyType.NotAKey).ToList();
        if (!columns.Any())
        {
            throw new ArgumentException("No columns were mapped.");
        }
        var tablename = generator.GetTableName(classMap);
        var columnNames = columns.Select(p => generator.GetColumnName(classMap, p, false));
        var parameters = columns.Select(p => generator.Configuration.Dialect.ParameterPrefix + p.Name);
        var valuesSetters = nonkeycolumns.Select(p => $"{generator.GetColumnName(classMap, p, false)}=@{p.Name}").ToList();
        var where = keys.AppendStrings(seperator: " and ");
        var sqlbuilder = new StringBuilder();
        sqlbuilder.AppendLine($"IF EXISTS (select * from {tablename} WITH (UPDLOCK, HOLDLOCK) WHERE ({where})) ");
        sqlbuilder.AppendLine(valuesSetters.Any() ? $"UPDATE {tablename} SET {valuesSetters.AppendStrings()} WHERE ({where}) " : "SELECT 0 ");
        sqlbuilder.AppendLine($"ELSE INSERT INTO {tablename} ({columnNames.AppendStrings()}) VALUES ({parameters.AppendStrings()}) ");
        return sqlbuilder.ToString();
    }
}


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué