Dapper предоставляет имя по умолчанию для наборов результатов Dynami c с QueryMultiple - PullRequest
1 голос
/ 19 февраля 2020

TLDR; Есть ли способ (с использованием карты типов или другого решения) дать dynamic результирующим наборам имя по умолчанию, такое как «(без имени столбца)» в Dapper, если имя столбца не указано?

I Я пишу редактор запросов, который позволяет пользователям писать и выполнять пользовательские запросы к базам данных MS SQL Server. Я использовал Dapper для всех наших запросов, и он прекрасно работал на 99% того, что нам нужно. Я наткнулся на загадку, и я надеюсь, что у кого-то есть решение.

Редактор запросов похож на SSMS. Я не знаю заранее, как будет выглядеть скрипт, какой будет форма или тип результирующих наборов или даже сколько результирующих наборов будет возвращено. По этой причине я пакетирую сценарии и использую Dapper's QueryMultiple для чтения dynamic результатов из GridReader. Затем результаты отправляются в стороннюю таблицу данных пользовательского интерфейса (WPF). Сетка данных знает, как использовать данные Dynami c, и единственное, что ей требуется для отображения данной строки, - это как минимум одна пара значений ключа с ненулевым, но необязательно уникальным ключом и значением, допускающим значение 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 ", "\r\nGO ", "\n\nGO ", "\nGO\n", "\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, который приводится к IDictionary<string,object>, и вы вызываете функцию Remove(x) (где x - это весь элемент ИЛИ просто ключ для любого отдельного элемента с нулевой или пустой ключ), все последующие попытки разрешить другие значения с нулевым или пустым ключом через индексатор item[key] не могут получить значение - даже если в объекте все еще существуют дополнительные пары значений ключа.

Другими словами, я не могу удалить или заменить последующие пустые ключи после удаления первого.

Я что-то упускаю из виду? Нужно ли мне просто изменить DapperRow с помощью отражения и надеяться, что у него нет каких-либо странных побочных эффектов или что базовая структура данных не изменится позже? Или я беру производительность / память и просто копирую / отображаю весь потенциально большой набор результатов в новую последовательность, чтобы дать пустым ключам значение по умолчанию во время выполнения?

1 Ответ

1 голос
/ 19 февраля 2020

Я подозреваю, что это потому, что динамический c DapperRow объект на самом деле не является "нормальным" словарем. Может иметь несколько записей с одним и тем же ключом. Вы можете увидеть это, если осмотрите объект в отладчике.

Когда вы ссылаетесь на rowDictionary[x.Key], я подозреваю, что вы всегда получите первый безымянный столбец.

Если вы позвоните rowDictionary.Remove(""); rowDictionary.Remove("");, вы фактически удаляете только первую запись - вторая все еще присутствует, даже если rowDictionary.ContainsKey("") возвращает false.

Вы можете Clear() и перестроить весь словарь. В этот момент вы на самом деле не получаете много, используя динамический c объект.

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);
        }
    }
}
...