iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
4
Software Development

🌊 進階學習 ADO.NET、Dapper、Entity Framework 系列 第 10

【深入Dapper.NET源碼】Dapper 效率快關鍵之一 : Cache 緩存原理

為何Dapper可以這麼快?

前面介紹到動態使用 Emit IL 建立 ADO.NET Mapping 方法,但單就這功能無法讓 Dapper 被稱為輕量ORM效率之王。

因為動態建立方法是需要成本、並耗費時間的動作,單純使用反而會拖慢速度。但當配合 Cache 後就不一樣,將建立好的方法保存在 Cache 內,可以用『空間換取時間』概念加快查詢的效率,也就是俗稱查表法

接著追蹤Dapper源碼,這次需要特別關注的是QueryImpl方法下的Identity、GetCacheInfo
https://ithelp.ithome.com.tw/upload/images/20191005/20105988cCwaS7ejnY.png

Identity、GetCacheInfo

Identity主要封裝各緩存的比較Key屬性 :

  • sql : 區分不同SQL字串
  • type : 區分Mapping類別
  • commandType : 負責區分不同資料庫
  • gridIndex : 主用用在QueryMultiple,後面講解。
  • connectionString : 主要區分同資料庫廠商但是不同DB情況
  • parametersType : 主要區分參數類別
  • typeCount : 主要用在Multi Query多映射,需要搭配override GetType方法,後面講解

接著搭配GetCacheInfo方法內Dapper使用的緩存類別ConcurrentDictionary<Identity, CacheInfo>,使用TryGetValue方法時會去先比對HashCode接著比對Equals特性,如圖片源碼。
https://ithelp.ithome.com.tw/upload/images/20191005/20105988tOgZiBCwly.png

將Key類別Identity藉由override Equals方法實現緩存比較算法,可以看到以下Dapper實作邏輯,只要一個屬性不一樣就會建立一個新的動態方法、緩存。

public bool Equals(Identity other)
{
	if (ReferenceEquals(this, other)) return true;
	if (ReferenceEquals(other, null)) return false;

	int typeCount;
	return gridIndex == other.gridIndex
		&& type == other.type
		&& sql == other.sql
		&& commandType == other.commandType
		&& connectionStringComparer.Equals(connectionString, other.connectionString)
		&& parametersType == other.parametersType
		&& (typeCount = TypeCount) == other.TypeCount
		&& (typeCount == 0 || TypesEqual(this, other, typeCount));
}

以此概念拿之前Emit版本修改成一個簡單Cache Demo讓讀者感受:

public class Identity
{
	public string sql { get; set; }
	public CommandType? commandType { get; set; }
	public string connectionString { get; set; }
	public Type type { get; set; }
	public Type parametersType { get; set; }
	public Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType)
	{
		this.sql = sql;
		this.commandType = commandType;
		this.connectionString = connectionString;
		this.type = type;
		this.parametersType = parametersType;
		unchecked
		{
			hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this
			hashCode = (hashCode * 23) + commandType.GetHashCode();
			hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0);
			hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0);
			hashCode = (hashCode * 23) + (connectionString == null ? 0 : StringComparer.Ordinal.GetHashCode(connectionString));
			hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0);
		}
	}

	public readonly int hashCode;
	public override int GetHashCode() => hashCode;
	
	public override bool Equals(object obj) => Equals(obj as Identity);
	public bool Equals(Identity other)
	{
		if (ReferenceEquals(this, other)) return true;
		if (ReferenceEquals(other, null)) return false;

		return type == other.type
			&& sql == other.sql
			&& commandType == other.commandType
			&& StringComparer.Ordinal.Equals(connectionString, other.connectionString)
			&& parametersType == other.parametersType;
	}
}

public static class DemoExtension
{
	private static readonly Dictionary<Identity, Func<DbDataReader, object>> readers = new Dictionary<Identity, Func<DbDataReader, object>>();

	public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql,object param=null) where T : new()
	{
		using (var command = cnn.CreateCommand())
		{
			command.CommandText = sql;
			using (var reader = command.ExecuteReader())
			{
				var identity = new Identity(command.CommandText, command.CommandType, cnn.ConnectionString, typeof(T), param?.GetType());
				
				// 2. 如果cache有資料就使用,沒有資料就動態建立方法並保存在緩存內
				if (!readers.TryGetValue(identity, out Func<DbDataReader, object> func))
				{
					//動態建立方法
					func = GetTypeDeserializerImpl(typeof(T), reader);
					readers[identity] = func;
					Console.WriteLine("沒有緩存,建立動態方法放進緩存");
				}else{
					Console.WriteLine("使用緩存");
				}


				// 3. 呼叫生成的方法by reader,讀取資料回傳
				while (reader.Read())
				{
					var result = func(reader as DbDataReader);
					yield return result is T ? (T)result : default(T);
				}
			}

		}
	}

	private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
	{
		//..略
	}
}

效果圖 :
https://ithelp.ithome.com.tw/upload/images/20191005/20105988mKud6Ejzqe.png


上一篇
【深入Dapper.NET源碼】Strongly Typed Mapping 原理 Part6 : Emit版本
下一篇
【深入Dapper.NET源碼】錯誤SQL字串拼接方式,會導致效率慢、內存洩漏
系列文
🌊 進階學習 ADO.NET、Dapper、Entity Framework 30

尚未有邦友留言

立即登入留言