Dapper使用QueryMultiple为动态结果集提供默认名称



TLDR;是否有方法(使用类型映射或其他解决方案(为dynamic结果集提供默认名称,例如在未提供列名的情况下,Dapper中的"(无列名("?

我正在编写一个查询编辑器,它允许用户针对MSSQLServer数据库编写和运行用户提供的查询。我一直在使用Dapper进行所有的查询,它可以很好地满足99%的需求。不过我遇到了一个障碍,我希望有人能找到解决方案。

查询编辑器类似于SSMS。我不知道脚本会是什么样子,结果集的形状或类型会是什么,甚至不知道会返回多少结果集。出于这个原因,我一直在批处理脚本,并使用Dapper的QueryMultipleGridReader中读取dynamic结果。然后将结果发送到第三方UI数据网格(WPF(。数据网格知道如何使用动态数据,显示给定行所需的唯一条件是至少有一个具有非null(但不一定是唯一的(键和可null值的键值对。到目前为止,一切都很好。

Dapper调用的简化版本如下所示:

public async Task<IEnumerable<IEnumerable<T>>> QueryMultipleAsync<T>(string sql, 
       object parameters, 
       string connectionString,
       CommandType commandType = CommandType.Text, 
       CancellationTokenSource cancellationTokenSource = null)
{
using (IDbConnection con = _dbConnectionFactory.GetConnection(connectionString))
{
con.Open();
var transaction = con.BeginTransaction();
var sqlBatches = sql
.ToUpperInvariant()
.Split(new[] { " GO ", "rnGO ", "nnGO ", "nGOn", "tGO ", "rGO "}, StringSplitOptions.RemoveEmptyEntries);
var batches = new List<CommandDefinition>();
foreach(var batch in sqlBatches)
{
batches.Add(new CommandDefinition(batch, parameters, transaction, null, commandType, CommandFlags.Buffered, cancellationTokenSource.Token));
}
var resultSet = new List<List<T>>();
foreach (var commandDefinition in batches)
{
using (GridReader reader = await con.QueryMultipleAsync(commandDefinition))
{
while (!reader.IsConsumed)
{
try
{
var result = (await reader.ReadAsync<T>()).AsList();
if (result.FirstOrDefault() is IDynamicMetaObjectProvider)
{
(result as List<dynamic>).ConvertNullKeysToNoColumnName();
}
resultSet.Add(result);
}
catch(Exception e)
{
if(e.Message.Equals("No columns were selected"))
{
break;
}
else
{
throw;
}
}
}
}
}
try
{
transaction.Commit();
}
catch (Exception ex)
{
Trace.WriteLine(ex.ToString());
if (transaction != null)
{
transaction.Rollback();
}
}
return resultSet;
}
}
public static IEnumerable<dynamic> ConvertNullKeysToNoColumnName<dynamic>(this IEnumerable<dynamic> rows)
{
foreach (var row in rows)
{
if (row is IDictionary<string, object> rowDictionary)
{
if (rowDictionary == null) continue;
rowDictionary.Where(x => string.IsNullOrEmpty(x.Key)).ToList().ForEach(x =>
{
var val = rowDictionary[x.Key];
if (x.Value == val)
{
rowDictionary.Remove(x);
rowDictionary.Add("(No Column Name)", val);
}
else
{
Trace.WriteLine("Something went wrong");
}
});
}
}
return rows;
}  

这适用于大多数查询(以及只有一个未命名结果列的查询(,但当用户编写一个包含多个未命名列的查询时,问题就会显现出来,如下所示:

select COUNT(*), MAX(create_date) from sys.databases

在这种情况下,Dapper返回一个DapperRow,它看起来像这样:

{DapperRow, = '9', = '2/14/2020 9:51:54 AM'}

因此,结果集正是用户所要求的(即,没有名称或别名的值(,但我需要为网格中的所有数据提供(非唯一(键。。。

我的第一个想法是简单地将DapperRow对象中的空键更改为默认值(如"(无列名("(,因为它似乎已针对存储进行了优化,因此表键只在对象中存储一次(这很好,并将为具有巨大结果集的查询提供很好的性能奖励(。但是DapperRow类型是私有的。在四处搜索后,我发现我可以将DapperRow转换为IDictionary<string, object>来访问对象的键和值,甚至可以设置和删除值。这就是ConvertNullKeysToNoColumnName扩展方法的来源。它有效。。。但只有一次。

为什么?看起来,当你在一个DapperRow中有多个null或空键,该键被转换为IDictionary<string,object>,并且你调用Remove(x)函数(其中x是整个项,或者只是任何一个具有null或空密钥的项的键(时,所有随后通过索引器item[key]用null或空键解析其他值的尝试都无法检索到值——即使对象中仍然存在附加的键值对。

换句话说,在移除第一个空键后,我无法移除或替换后续的空键。

我是不是错过了一些显而易见的东西?我只需要通过反射来更改DapperRow,并希望它不会有任何奇怪的副作用,或者底层数据结构以后不会改变吗?或者,我应该考虑性能/内存命中率,然后将整个潜在的大型结果集复制/映射到一个新序列中,以便在运行时为空键提供默认值?

我怀疑这是因为动态DapperRow对象实际上不是一个"正常"字典。它可以有多个具有相同键的条目。如果在调试器中检查对象,您可以看到这一点。

当您引用rowDictionary[x.Key]时,我怀疑您总是会得到第一个未命名的列。

如果调用rowDictionary.Remove(""); rowDictionary.Remove("");,实际上只删除第一个条目——即使rowDictionary.ContainsKey("")返回false,第二个条目仍然存在。

可以Clear()并重建整个字典。在这一点上,使用动态对象实际上并没有获得太多好处。

if (row is IDictionary<string, object>)
{
var rowDictionary = row as IDictionary<string, object>;
if (rowDictionary.ContainsKey(""))
{
var kvs = rowDictionary.ToList();
rowDictionary.Clear();
for (var i = 0; i < kvs.Count; ++i)
{
var kv = kvs[i];
var key = kv.Key == ""? $"(No Column <{i + 1}>)" : kv.Key;
rowDictionary.Add(key, kv.Value);
}
}
}

由于您正在处理未知的结果结构,并且只想将其传递给网格视图,因此我会考虑使用DataTable。

您仍然可以保留Dapper进行参数处理:

foreach (var commandDefinition in batches)
{
using(var reader = await con.ExecuteReaderAsync(commandDefinition)) {
while(!reader.IsClosed) {
var table = new DataTable();
table.Load(reader);
resultSet.Add(table);
}
}
}

最新更新