当前位置: 首页 > 工具软件 > Dapper > 使用案例 >

Dapper的效率问题的总结

申宜
2023-12-01

前言:

Dapper是一款非常方便的轻量级的ORM工具,
这里放一个它的文档:
Dapper帮助文档
它拓展了IConnection接口的方法,使其能够查询Model的List,不需要提前的Mapper设置,也支持多种写入参数的方式
与其他ORM框架相比,确实很方便.
但在实际的使用中,当数据量很大时,它的执行效率比原生的ADO.NET低了很多倍.

问题的发现

有需求要把大量的数据查出然后导入ExceL中,
开始是使用的Dapper进行List查询,大概在执行上花了2分钟左右(只是需要的数据的一部分)
后来改做DataTable查询,执行的时间并没有快多少,
最后换了原生的ADO.NET的方式进行查询,
在时间上直接进入10秒以内

这种差距感到很惊讶,故准备进Dapper的源码进行调试
顺便测试一下

測試

在三行不同的查询上打上断点

List<POCO> selList = _SqlDapper.QueryList<POCO>(SQL,null);//这个QueryList是自行封装的,故不显示其内容

DataTable Test = _SqlDapper.QueryDataTable(SQL,null);

DataTable dt = SQLCommand.ExecuteDataTable(SQL);

(在工具---->设置—>Debuger里把Just My Code给关闭,不然Debug不会进去,至于它会要求链接或下载源码可以不管,点击反编译选项即可)

1.第一条语句进入的Dapper方法:

单步1,进入数据库的连接配置

public ProfiledDbConnection(DbConnection connection, IDbProfiler profiler)
{
	_connection = (connection ?? throw new ArgumentNullException("connection"));
	_connection.StateChange += StateChangeHandler;
	if (profiler != null)
	{
		_profiler = profiler;
	}
}

此处源码进行了数据库的连接配置,和查询关联不大,
单步2,查询前的参数配置等等…

public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
{
	CommandDefinition command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None);
	//配置SQL语句,参数,事务,超时时间等等
	IEnumerable<T> data = cnn.QueryImpl<T>(command, typeof(T));  //真正的查询在这里
	if (!command.Buffered)
	{
		return data;
	}
	return data.ToList();
}

这一步是主要配置参数

单步3,执行SQL进行查询,及类型转化

private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefinition command, Type effectiveType)
{
	//获取参数
	object param = command.Parameters;
	//验证Sql和参数等的信息
	Identity identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType());
	//设置缓存
	CacheInfo info = GetCacheInfo(identity, param, command.AddToCache);
	IDbCommand cmd = null;
	IDataReader reader = null; //Attention Here 它是使用DataReader来进行数据的读取
	//这里就是问题的所在
	bool wasClosed = cnn.State == ConnectionState.Closed;
	try
	{
		cmd = command.SetupCommand(cnn, info.ParamReader);
		if (wasClosed)
		{
			cnn.Open();
		}
		//在这里执行了SQL语句,这个方法只是把查询简单的包装一下,我把它放到下面
		reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, CommandBehavior.SingleResult | CommandBehavior.SequentialAccess);
		(  //这就是那个方法,它不在这个位置,我为了方便把它放到里面来
					private static IDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool wasClosed, CommandBehavior behavior)
					{
					try
					{   //执行SQL,        
						return cmd.ExecuteReader(GetBehavior(wasClosed, behavior));
					}
					catch (ArgumentException ex)
					{
						if (Settings.DisableCommandBehaviorOptimizations(behavior, ex))
						{
							//执行SQL
							return cmd.ExecuteReader(GetBehavior(wasClosed, behavior));
						}
						throw;
					}
				}
		)
		wasClosed = false;
		DeserializerState tuple = info.Deserializer; //设置反序列化器
		int hash = GetColumnHash(reader);
		if (tuple.Func != null && tuple.Hash == hash)
		{
			goto IL_0174;     //这个地方应该是反编译的问题,没有正常显示(无关紧要)
		}
		if (reader.FieldCount != 0)
		{
			//获取反序列化状态
			DeserializerState deserializerState2 = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, returnNullIfFirstMissing: false));
			tuple = deserializerState2;
			if (command.AddToCache)
			{	//设置重庆讯缓存
				SetQueryCache(identity, info);
			}
			goto IL_0174;
		}
		goto end_IL_00a0;
		IL_0174:
		//设置反序列化的方法委托
		Func<IDataReader, object> func = tuple.Func;
		Type convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
		while (reader.Read())  //直接去遍历了整个DataReader
		{
			object val = func(reader);   //读取'一行'数据,利用方法委托转化为Object对象
			if (val == null || val is T)
			{
				yield return (T)val;   //再对Obj进行强转
			}
			else
			{
				//再对Obj进行强转
				yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture);
			}
		}
		while (reader.NextResult())  //检查DataReader是否处理完
		{
		}
		reader.Dispose();
		reader = null;
		command.OnCompleted();
		end_IL_00a0:;
	}
	finally	   	//关闭连接操作
	{
		if (reader != null)  
		{
			if (!reader.IsClosed)
			{
				try
				{
					cmd.Cancel();
				}
				catch
				{
				}
			}
			reader.Dispose();
		}
		if (wasClosed)
		{
			cnn.Close();
		}
		cmd?.Dispose();
	}
}

2.第二条语句进入的Dapper方法:

由于Dapper没有返回DataTable的方法,这条语句实际上是把ExecuteReader的dataReader结果转型的DataTable,故效率和上一种的方法差别应该不大,单页需要进入查看一下步骤
单步1:还是用DataReader执行的

public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
	CommandDefinition command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType);
	IDbCommand dbcmd;
	IDataReader reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out dbcmd);
	return WrappedReader.Create(dbcmd, reader);
}

单步进入执行方法:

private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand cmd)
{	//因为返回的是DataReader,不需要去处理类型信息,所以不需要(正反)序列化,类型检查,强制转型...
	//因此效率比上一个方法要好一点
	Action<IDbCommand, object> paramReader = GetParameterReader(cnn, ref command);
	cmd = null;
	bool wasClosed = cnn.State == ConnectionState.Closed;
	bool disposeCommand = true;
	try
	{
		cmd = command.SetupCommand(cnn, paramReader);
		if (wasClosed)
		{
			cnn.Open();
		}
		IDataReader reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, commandBehavior);
		wasClosed = false;
		disposeCommand = false;
		return reader;
	}
	finally
	{
		if (wasClosed)
		{
			cnn.Close();
		}
		if (cmd != null && disposeCommand)
		{
			cmd.Dispose();
		}
	}
}

3.第三条语句进入的方法:

第三条语句是微软自己的工具,VS当然反编译不了,但它很简单,
只是用原生的DataAdapter.Fill(DataTable table)

SqlDataAdapter adapter = new SqlDataAdapter();  //大概就是使用DataAdapter.F
adapter.SelectCommand = cmd;
adapter.Fill(DataTable);  

结果:

执行的时间对比(数据量10w+)
3>2>1
时间大致为
7s : 40s : 50s+

原因

可以很明显的看出Dapper单单使用DataReader,而原生的ADO.NET中用的是DataAdapter
原因归根结底还是DataAdapter和DataReader的适用性问题

根据查到的信息,DataAdapter.Fill()是短连接,一次加载所有数据到DataTable中,
而DataReader是长连接,一行一行的读取数据库查询信息,(通常在一个while循环里读取,这点像JDBC的ResultSet),这样就很容易进行ORM操作,好做类型的转化,加之Dapper的参数设置,类型检查等等,反序列化操作等等…导致了大量数据查询时的效率低下.

这里给一篇作参考的文章
https://blog.csdn.net/u012927285/article/details/44095195

 类似资料: