如何避免使用Dapper.Contrib在后续的Update调用中更新列?

时间:2018-10-04 00:03:08

标签: c# dapper dapper-contrib

我有一个类Media,与我的数据库表Media大多是一对一的。我通过传递Update对象,使用Dapper.Contrib的SqlConnection Media扩展方法更新表。

我表的一列(以及相应的类属性)是OwnerID,简单来说,它就是最初创建此媒体的用户的ID。它应该写在第一个插入上(也可以使用Dapper.Contrib的Insert扩展方法完成),然后更新就不能更改它。是否可以仅使用Dapper.Contrib完成此操作?如果可能的话,我不想在进行更新之前先阅读OwnerID列,只是为了确保OwnerID对象的属性相同。

[Computed]属性似乎从UpdateInsert都省略了该列。描述here似乎表明该属性仅应省略对更新的列的写入。让我希望我只是错误地使用了库。

1 个答案:

答案 0 :(得分:1)

不幸的是,Dapper.Contrib无法做您想要的事情。关键是,根据sources扩展方法Insert<T>Update<T>具有完全相同的机制来收集字段,这将受到影响。基本上看起来像:

// in Insert<T>
var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList();

...

// in Update<T>
var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList();

因此,这些方法始终会影响同一组字段。要实现目标行为,您需要编写自己的扩展名和属性。下面有很多代码,我不认为这是一种优雅的解决方案。但是,您可以根据需要自由使用和重新加工它。它是一个粗略的同步实现,因此,请以它为例,以帮助您开发自己的解决方案。这是自定义MyUpdate<T>扩展方法的测试示例:

[Table("Media")]
public class Media
{
    [Key]
    public long Id { get; set; }

    [NotUpdateable]
    public int OwnerId { get; set; }

    public string Name { get; set; }
}

[Test]
public void DapperContribNotWriteableField()
{
    // Arrange
    var conn = new SqlConnection(
        "Data Source=vrpisilstage.c0hnd1p1buwt.us-east-1.rds.amazonaws.com;Initial Catalog=VRPISIL;User ID=VRPISILSTAGE;Password=ottubansIvCajlokojOt;Connect Timeout=100");
    conn.Open();
    var media = new Media
    {
        OwnerId = 100500,
        Name = "Media"
    };

    // Act
    media.Id = conn.Insert(media);
    media.OwnerId = 500100;
    conn.MyUpdate(media);
    var result = conn.Get<Media>(media.Id);

    // Assert
    Assert.AreEqual(result.OwnerId, 100500);
}

数据库中的表

CREATE TABLE [Media]
(
    [Id]           INT IDENTITY (1, 1) NOT NULL,
    [OwnerId]      BIGINT              NOT NULL,    
    [Name]         VARCHAR(50)         NOT NULL
)

这里是标记属性的属性,在更新查询中不能使用:

/// <summary>
/// Specifies that this is a not updateable column.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class NotUpdateableAttribute : Attribute
{
}

这是经过重新设计的扩展方法,其中考虑了属性NotUpdatable

/// <summary>
/// My extensions for Dapper
/// </summary>
public static class TestSqlMapperExtensions
{
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> KeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> ExplicitKeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> TypeProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> ComputedProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> UpdatableProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> GetQueries = new ConcurrentDictionary<RuntimeTypeHandle, string>();
    private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeTableName = new ConcurrentDictionary<RuntimeTypeHandle, string>();

    private static readonly ISqlAdapter DefaultAdapter = new SqlServerAdapter();
    private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary
        = new Dictionary<string, ISqlAdapter>
        {
            ["sqlconnection"] = new SqlServerAdapter(),
            ["sqlceconnection"] = new SqlCeServerAdapter(),
            ["npgsqlconnection"] = new PostgresAdapter(),
            ["sqliteconnection"] = new SQLiteAdapter(),
            ["mysqlconnection"] = new MySqlAdapter(),
            ["fbconnection"] = new FbAdapter()
        };

    private static List<PropertyInfo> ComputedPropertiesCache(Type type)
    {
        if (ComputedProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
        {
            return pi.ToList();
        }

        var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList();

        ComputedProperties[type.TypeHandle] = computedProperties;
        return computedProperties;
    }

    private static List<PropertyInfo> NotUpdateablePropertiesCache(Type type)
    {
        if (UpdatableProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
        {
            return pi.ToList();
        }

        var notUpdateableProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is NotUpdateableAttribute)).ToList();

        UpdatableProperties[type.TypeHandle] = notUpdateableProperties;
        return notUpdateableProperties;
    }

    private static List<PropertyInfo> ExplicitKeyPropertiesCache(Type type)
    {
        if (ExplicitKeyProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
        {
            return pi.ToList();
        }

        var explicitKeyProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)).ToList();

        ExplicitKeyProperties[type.TypeHandle] = explicitKeyProperties;
        return explicitKeyProperties;
    }

    private static List<PropertyInfo> KeyPropertiesCache(Type type)
    {
        if (KeyProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pi))
        {
            return pi.ToList();
        }

        var allProperties = TypePropertiesCache(type);
        var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute)).ToList();

        if (keyProperties.Count == 0)
        {
            var idProp = allProperties.Find(p => string.Equals(p.Name, "id", StringComparison.CurrentCultureIgnoreCase));
            if (idProp != null && !idProp.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute))
            {
                keyProperties.Add(idProp);
            }
        }

        KeyProperties[type.TypeHandle] = keyProperties;
        return keyProperties;
    }

    private static List<PropertyInfo> TypePropertiesCache(Type type)
    {
        if (TypeProperties.TryGetValue(type.TypeHandle, out IEnumerable<PropertyInfo> pis))
        {
            return pis.ToList();
        }

        var properties = type.GetProperties().Where(IsWriteable).ToArray();
        TypeProperties[type.TypeHandle] = properties;
        return properties.ToList();
    }

    private static bool IsWriteable(PropertyInfo pi)
    {
        var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList();
        if (attributes.Count != 1) return true;

        var writeAttribute = (WriteAttribute)attributes[0];
        return writeAttribute.Write;
    }



    private static string GetTableName(Type type)
    {
        if (TypeTableName.TryGetValue(type.TypeHandle, out string name)) return name;

        if (SqlMapperExtensions.TableNameMapper != null)
        {
            name = SqlMapperExtensions.TableNameMapper(type);
        }
        else
        {
            var info = type;
            //NOTE: This as dynamic trick falls back to handle both our own Table-attribute as well as the one in EntityFramework 
            var tableAttrName =
                info.GetCustomAttribute<TableAttribute>(false)?.Name
                ?? (info.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic)?.Name;

            if (tableAttrName != null)
            {
                name = tableAttrName;
            }
            else
            {
                name = type.Name + "s";
                if (type.IsInterface && name.StartsWith("I"))
                    name = name.Substring(1);
            }
        }

        TypeTableName[type.TypeHandle] = name;
        return name;
    }

    /// <summary>
    /// Updates entity in table "Ts", checks if the entity is modified if the entity is tracked by the Get() extension.
    /// </summary>
    /// <typeparam name="T">Type to be updated</typeparam>
    /// <param name="connection">Open SqlConnection</param>
    /// <param name="entityToUpdate">Entity to be updated</param>
    /// <param name="transaction">The transaction to run under, null (the default) if none</param>
    /// <param name="commandTimeout">Number of seconds before command execution timeout</param>
    /// <returns>true if updated, false if not found or not modified (tracked entities)</returns>
    public static bool MyUpdate<T>(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
    {
        if (entityToUpdate is Dapper.Contrib.Extensions.SqlMapperExtensions.IProxy proxy && !proxy.IsDirty)
        {
            return false;
        }

        var type = typeof(T);

        if (type.IsArray)
        {
            type = type.GetElementType();
        }
        else if (type.IsGenericType)
        {
            var typeInfo = type.GetTypeInfo();
            bool implementsGenericIEnumerableOrIsGenericIEnumerable =
                typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
                typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>);

            if (implementsGenericIEnumerableOrIsGenericIEnumerable)
            {
                type = type.GetGenericArguments()[0];
            }
        }

        var keyProperties = KeyPropertiesCache(type).ToList();  //added ToList() due to issue #418, must work on a list copy
        var explicitKeyProperties = ExplicitKeyPropertiesCache(type);
        if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0)
            throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property");

        var name = GetTableName(type);

        var sb = new StringBuilder();
        sb.AppendFormat("update {0} set ", name);

        var allProperties = TypePropertiesCache(type);
        keyProperties.AddRange(explicitKeyProperties);
        var computedProperties = ComputedPropertiesCache(type);

        // Exclude not updateable fields
        var notUpdateableProperties = NotUpdateablePropertiesCache(type);
        var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties).Union(notUpdateableProperties)).ToList();

        var adapter = GetFormatter(connection);

        for (var i = 0; i < nonIdProps.Count; i++)
        {
            var property = nonIdProps[i];
            adapter.AppendColumnNameEqualsValue(sb, property.Name);  //fix for issue #336
            if (i < nonIdProps.Count - 1)
                sb.Append(", ");
        }
        sb.Append(" where ");
        for (var i = 0; i < keyProperties.Count; i++)
        {
            var property = keyProperties[i];
            adapter.AppendColumnNameEqualsValue(sb, property.Name);  //fix for issue #336
            if (i < keyProperties.Count - 1)
                sb.Append(" and ");
        }
        var updated = connection.Execute(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction);
        return updated > 0;
    }

    private static ISqlAdapter GetFormatter(IDbConnection connection)
    {
        var name = SqlMapperExtensions.GetDatabaseType?.Invoke(connection).ToLower()
                   ?? connection.GetType().Name.ToLower();

        return !AdapterDictionary.ContainsKey(name)
            ? DefaultAdapter
            : AdapterDictionary[name];
    }


}

希望有帮助。