從0開始編寫dapper核心功能、壓榨性能、自己動手豐衣足食


我偶然聽說sqlsugar的性能比dapper強。對此我表示懷疑(由於我一直使用的dapper存在偏見吧),於是自己測試了sqlsugar、freesql、dapper發現他們的給我的結果是

sqlsugar>dapper>freesql(這里並不是黑那個orm,畢竟不同orm功能不同,底層實現不同,適用場景不同性能當然不同)。這讓我很吃驚,dapper(號稱orm king)一個執行sql的映射器,比不了基於linq語法的sqlsugar。同時也讓我感到高興,我高興的是:orm的性能肯定還有提升的空間。

於是我便開始研究它們並着手編寫。最終以一千行左右的代碼量實現了dapper的基本映射功能,性能真正意義接近ado.net

對比於dapper的底層存在拆裝箱操作(我的沒有,請看IL),強制類型轉換,dapper內置各種緩存(緩存就要考慮並發安全,就要用lock),許多功能並不是我們所需要的,一些功能又是我們迫切需要的,dapper有些定制化功能我們要查閱很多資料才能實現。浪費我們寶貴的時間,dapper對匿名類型支持並不好,這阻礙的我的另一個框架dapper.common(dapper的linq實現方案,將來要移植到sqlcommon),我讓作者改一下,支持一下,作者認為linq映射也不是dapper所需要的功能,不予支持。

自己動手豐衣足食,那么我們完全可以自己編寫一套。

性能測試

 

 

 

 

下面進行簡要實現:

完整源碼地址:https://github.com/1448376744/SqlCommon

nuget也發布了v1.0.0

我們要如何實現?我們只需要實現DataReader對象轉實體類。我們需要用IL來動態創建下面的函數

public T TypeConvert<T>(IDataReader reader)
{
    var entity = new T();
    var index1 = reader.GetOrdinal("FieldName1");
    entity.FieldName1 = reader.GetString(index1);
    var index2 = reader.GetOrdinal("FieldName2");
    entity.FieldName2 = reader.GetString(index2);
    return entity;
}

我們可以創建這樣的函數,通過IL來動態創建,大致的過程

創建實體類型->判斷實體類型中的屬性在reader中是否存在->如果存在則對該字段賦值

我們定義一個接口,這個接口規范屬性和字段的映射規則,類型轉換規則,構造器規則,構造參數映射規則(可以有不同實現)

public interface ITypeMapper
{
    //根據字段信息,返回C#屬性
    MemberInfo FindMember(MemberInfo[] properties, DbDataInfo dataInfo);
    //根據C#字段屬性返回轉換函數
    MethodInfo FindConvertMethod(Type csharpType);
    //處理匿名類型
    DbDataInfo FindConstructorParameter(DbDataInfo[] dataInfos, ParameterInfo parameterInfo);
    //根據目標類型返回構造器
    ConstructorInfo FindConstructor(Type csharpType);
}

我們編寫一個默認實現規則

public class TypeMapper : ITypeMapper
{
    //查找構造器 public ConstructorInfo FindConstructor(Type csharpType)
    {
        var constructor = csharpType.GetConstructor(Type.EmptyTypes);
        if (constructor == null)
        {
            var constructors = csharpType.GetConstructors();
            constructor = constructors.Where(a => a.GetParameters().Length == constructors.Max(s => s.GetParameters().Length)).FirstOrDefault();
        }
        return constructor;
    }
//構造參數映射規則
public DbDataInfo FindConstructorParameter(DbDataInfo[] dataInfos, ParameterInfo parameterInfo) { foreach (var item in dataInfos) { if (item.DataName.Equals(parameterInfo.Name, StringComparison.OrdinalIgnoreCase)) { return item; } else if (SqlMapper.MatchNamesWithUnderscores && item.DataName.Replace("_", "").Equals(parameterInfo.Name, StringComparison.OrdinalIgnoreCase)) { return item; } } return null; }
//查找屬性
public MemberInfo FindMember(MemberInfo[] properties, DbDataInfo dataInfo) { foreach (var item in properties) { if (item.Name.Equals(dataInfo.DataName, StringComparison.OrdinalIgnoreCase)) { return item;//忽略大小寫 } else if (SqlMapper.MatchNamesWithUnderscores && item.Name.Equals(dataInfo.DataName.Replace("_", ""), StringComparison.OrdinalIgnoreCase)) { return item;//忽略下划線 } } return null; }
//查找類型轉換規則
public MethodInfo FindConvertMethod(Type csharpType) { if (csharpType == typeof(int) || Nullable.GetUnderlyingType(csharpType) == typeof(int)) { return csharpType == typeof(int) ? DataConvertMethod.ToIn32Method : DataConvertMethod.ToIn32NullableMethod; } } }

然后實現一下DataConvertMethod(FindConvertMethod需要)這里是縮減版

//你可以在這里編寫json類型的轉換策略,如果你的屬性中有JObject類型的話
public
static class DataConvertMethod { /// <summary> /// int轉換方法 /// </summary> public static MethodInfo ToIn32Method = typeof(DataConvertMethod).GetMethod(nameof(DataConvertMethod.ConvertToInt32)); /// <summary> /// int?轉換方法 /// </summary> public static MethodInfo ToIn32NullableMethod = typeof(DataConvertMethod).GetMethod(nameof(DataConvertMethod.ConvertToInt32Nullable)); public static int ConvertToInt32(this IDataRecord dr, int i) { if (dr.IsDBNull(i)) { return default; } var result = dr.GetInt32(i); return result; } public static int? ConvertToInt32Nullable(this IDataRecord dr, int i) { if (dr.IsDBNull(i)) { return default; } var result = dr.GetInt32(i); return result; } }

然后我們編寫IL來創建動態函數,並使用用上面的接口作為參數

private static Func<IDataRecord, T> CreateTypeSerializerHandler<T>(ITypeMapper mapper, IDataRecord record)
{
    var type = typeof(T);
    var dynamicMethod = new DynamicMethod($"{type.Name}Deserializer{Guid.NewGuid().ToString("N")}", type, new Type[] { typeof(IDataRecord) }, type, true);
    var generator = dynamicMethod.GetILGenerator();
    LocalBuilder local = generator.DeclareLocal(type);
    //獲取到這個record中的所有字段信息
    var dataInfos = new DbDataInfo[record.FieldCount];
    for (int i = 0; i < record.FieldCount; i++)
    {
        var dataname = record.GetName(i);
        var datatype = record.GetFieldType(i);
        var typename = record.GetDataTypeName(i);
        dataInfos[i] = new DbDataInfo(i, typename, datatype, dataname);
    }
    //查找構造器
    var constructor = mapper.FindConstructor(type);
    //獲取所有屬性
    var properties = type.GetProperties();
    //var entity = new T();
    generator.Emit(OpCodes.Newobj, constructor);
    generator.Emit(OpCodes.Stloc, local);
    //綁定屬性
    foreach (var item in dataInfos)
    {
        //根據屬性查找規則查找屬性,如果不存在則不綁定
        var property = mapper.FindMember(properties, item) as PropertyInfo;
        if (property == null)
        {
            continue;
        }
        //獲取轉換成該字段類型的轉換函數
        var convertMethod = mapper.FindConvertMethod(property.PropertyType);
        if (convertMethod == null)
        {
            continue;
        }
        //獲取該C#字段,在本次查詢的索引位
        int i = record.GetOrdinal(item.DataName);
        //下面這幾行IL的意思是
        //entity.FieldName1 = reader.ConvertToInt32(i);
        generator.Emit(OpCodes.Ldloc, local);
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldc_I4, i);
        if (convertMethod.IsVirtual)
            generator.Emit(OpCodes.Callvirt, convertMethod);
        else
            generator.Emit(OpCodes.Call, convertMethod);
        generator.Emit(OpCodes.Callvirt, property.GetSetMethod());
    }
    // return entity;
    generator.Emit(OpCodes.Ldloc, local);
    generator.Emit(OpCodes.Ret);
    //創建成委托,參數IDataReader,返回T,
    return dynamicMethod.CreateDelegate(typeof(Func<IDataRecord, T>)) as Func<IDataRecord, T>;
}

動態創建的IL綁定函數我們需要編寫一個緩存策略(我們使用hash結構進行存儲),一個目標類型可能生成多個綁定函數,這根據你sql返回的字段個數和順序有關

定義hash結構的key

private struct SerializerKey : IEquatable<SerializerKey>
{
    private string[] Names { get; set; }
    private Type Type { get; set; }
    public override bool Equals(object obj)
    {
        return obj is SerializerKey && Equals((SerializerKey)obj);
    }
//由於我們會查詢不同個數的列,而使用同一個實體,個數不同生成的綁定IL函數也不同
//所以同一個類型可能會生成多個綁定,因此重寫equals
public bool Equals(SerializerKey other) {
//先判斷目標類型
if (Type != other.Type) { return false; }
//判斷字段個數
else if (Names.Length != other.Names.Length) { return false; }
//判斷順序
else { for (int i = 0; i < Names.Length; i++) { if (Names[i] != other.Names[i]) { return false; } } return true; } } //根據類型進行hash存儲。 public override int GetHashCode() { return Type.GetHashCode(); } public SerializerKey(Type type, string[] names) { Type = type; Names = names; } }

編寫一個緩存策略

/// <summary>
/// 從緩存中讀取類型轉換器
/// </summary>
public static Func<IDataRecord, T> GetSerializer<T>(ITypeMapper mapper, IDataRecord record)
{
    string[] names = new string[record.FieldCount];
    for (int i = 0; i < record.FieldCount; i++)
    {
        names[i] = record.GetName(i);
    }
//從緩存中讀取
var key = new SerializerKey(typeof(T), names); _serializers.TryGetValue(key, out object handler); if (handler == null) {
//這里在寫的時候才開始lock,而dapper是在讀的時候,我認為那樣對並發有影響,不能因為你的框架要做緩存,就影響到我並發
//而我在寫的時候才鎖,只影響你第一次
lock (_serializers) { handler = CreateTypeSerializerHandler<T>(mapper, record); if (!_serializers.ContainsKey(key)) { _serializers.Add(key, handler); } } } return handler as Func<IDataRecord, T>; }

 

好了大部分工作都完成了,我們編一個sql執行器(簡化版)

public static IEnumerable<T> ExecuteQuery<T>(this IDbConnection connection, string sql)
{
    if (connection.State == ConnectionState.Closed)
        connection.Open();
    using (var cmd = connection.CreateCommand())
    {
        cmd.CommandText = sql;
        using (var reader = cmd.ExecuteReader())
        {
            var handler = TypeConvert.GetSerializer<T>(reader);
            while (reader.Read())
            {
                yield return handler(reader);
            }
        }
    }
}

至此我們已經完成了整個流程。

我們可以發現沒有拆裝箱,沒有強制類型轉換,

對比於使用ado.net的性能差距,由於我們的動態生成綁定函數,在下次使用的時候我們需要從hash表中去查詢這個函數指針。

這便是性能的差距點,而我們首先綁定函數,下次時候的時候顯示的調用你定義的綁定函數。

也就是說,你只要能優化這個緩存策略,就能無限接近手寫ado.net。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM