В этом посте показано, как запросить сильно нормализованную базу данных SQL и отобразить результат в набор сильно вложенных объектов C # POCO.
Состав:
- 8 строк C #.
- Некоторый достаточно простой SQL, который использует некоторые объединения.
- Две замечательные библиотеки.
Понимание, которое позволило мне решить эту проблему, заключается в том, чтобы отделить MicroORM
от mapping the result back to the POCO Entities
. Таким образом, мы используем две отдельные библиотеки:
По сути, мы используем Dapper для запроса базы данных, затем используем Slapper.Automapper , чтобы отобразить результат прямо в наши POCO.
Преимущества
- Простота . Его менее 8 строк кода. Я нахожу это намного проще для понимания, отладки и изменения.
- Меньше кода . Несколько строк кода - это все Slapper.Automapper должен обрабатывать все, что вы на него бросаете, даже если у нас есть комплексное вложенное POCO (то есть POCO содержит
List<MyClass1>
, который, в свою очередь, содержит List<MySubClass2>
и т. Д.) .
- Скорость . Обе эти библиотеки обладают невероятным объемом оптимизации и кэширования, благодаря чему они выполняются почти так же быстро, как и настраиваемые вручную запросы ADO.NET.
- Разделение интересов . Мы можем поменять MicroORM на другой, и отображение все еще работает, и наоборот.
- Гибкость . Slapper.Automapper обрабатывает произвольно вложенные иерархии, не ограничивается парой уровней вложенности. Мы можем легко внести быстрые изменения, и все будет работать.
- Debugging . Сначала мы видим, что SQL-запрос работает правильно, затем мы можем проверить, что результат SQL-запроса правильно сопоставлен с целевыми объектами POCO.
- Простота разработки на SQL . Я считаю, что создание плоских запросов с
inner joins
для возврата плоских результатов намного проще, чем создание множественных операторов выбора с прошивкой на стороне клиента.
- Оптимизированные запросы в SQL . В сильно нормализованной базе данных создание плоского запроса позволяет механизму SQL применять расширенные оптимизации ко всему, что, как правило, было бы невозможно, если бы было создано и выполнено много небольших отдельных запросов.
- Trust . Dapper - это серверная часть StackOverflow, и Рэнди Берден - суперзвезда. Должен ли я сказать больше?
- Скорость разработки. Мне удалось выполнить несколько чрезвычайно сложных запросов со многими уровнями вложенности, и время разработки было довольно низким.
- Меньше ошибок. Я написал это однажды, это просто сработало, и теперь эта техника помогает питать компанию FTSE. Было так мало кода, что не было неожиданного поведения.
Недостатки
- Масштабирование превышает 1 000 000 возвращаемых строк. Хорошо работает при возврате <100 000 строк. Однако, если мы возвращаем> 1 000 000 строк, чтобы уменьшить трафик между нами и сервером SQL, мы не должны выравнивать его с помощью
inner join
(который возвращает дубликаты), мы должны вместо этого использовать несколько операторов select
и сшить все вместе на стороне клиента (см. другие ответы на этой странице).
- Этот метод ориентирован на запросы . Я не использовал этот метод для записи в базу данных, но я уверен, что Dapper более чем способен сделать это с некоторой дополнительной работой, так как сам StackOverflow использует Dapper в качестве своего уровня доступа к данным (DAL).
Тестирование производительности
В моих тестах Slapper.Automapper добавил небольшие издержки к результатам, возвращаемым Dapper, что означало, что он все еще был в 10 раз быстрее, чем Entity Framework, и комбинация все еще довольно чертовски близка до теоретической максимальной скорости SQL + C # способен .
В большинстве практических случаев большая часть накладных расходов будет приходиться на неоптимальный SQL-запрос, а не на некоторое отображение результатов на стороне C #.
Результаты тестирования производительности
Общее количество итераций: 1000
Dapper by itself
: 1.889 миллисекунд на запрос, используя 3 lines of code to return the dynamic
.
Dapper + Slapper.Automapper
: 2,446 миллисекунд на запрос, используя дополнительные 3 lines of code for the query + mapping from dynamic to POCO Entities
.
Рабочий пример
В этом примере у нас есть список Contacts
, и у каждого Contact
может быть один или несколько phone numbers
.
POCO Entities
public class TestContact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public List<TestPhone> TestPhones { get; set; }
}
public class TestPhone
{
public int PhoneId { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
}
Таблица SQL TestContact
Таблица SQL TestPhone
Обратите внимание, что эта таблица имеет внешний ключ ContactID
, который ссылается на таблицу TestContact
(это соответствует List<TestPhone>
в POCO выше).
SQL, который дает плоский результат
В нашем запросе SQL мы используем столько операторов JOIN
, сколько нам нужно, чтобы получить все необходимые данные в плоской денормализованной форме . Да, это может привести к дублированию в выходных данных, но эти дубликаты будут удалены автоматически, когда мы используем Slapper.Automapper , чтобы автоматически отобразить результат этого запроса прямо в нашу карту объектов POCO.
USE [MyDatabase];
SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId
код C #
const string sql = @"SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";
string connectionString = // -- Insert SQL connection string here.
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
// Can set default database here with conn.ChangeDatabase(...)
{
// Step 1: Use Dapper to return the flat result as a Dynamic.
dynamic test = conn.Query<dynamic>(sql);
// Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
// - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
// let it know the primary key for each POCO.
// - Must also use underscore notation ("_") to name parameters in the SQL query;
// see Slapper.Automapper docs.
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });
var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();
foreach (var c in testContact)
{
foreach (var p in c.TestPhones)
{
Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);
}
}
}
}
выход
POCO Entity Hierarchy
Глядя в Visual Studio, мы видим, что Slapper.Automapper правильно заполнил наши POCO-сущности, то есть у нас есть List<TestContact>
, а у каждого TestContact
есть List<TestPhone>
.
Примечания
И Dapper, и Slapper.Automapper кэшируют все внутри для скорости. Если у вас возникают проблемы с памятью (очень маловероятно), убедитесь, что вы иногда очищаете кеш для них обоих.
Убедитесь, что вы называете возвращающиеся столбцы, используя знак подчеркивания (_
) , чтобы дать Slapper.Automapper подсказки о том, как сопоставить результат с объектами POCO.
Убедитесь, что вы указали Slapper.Automapper ключи на первичном ключе для каждого объекта POCO (см. Строки Slapper.AutoMapper.Configuration.AddIdentifiers
). Для этого вы также можете использовать Attributes
в POCO. Если вы пропустите этот шаг, то он может пойти не так (теоретически), поскольку Slapper.Automapper не будет знать, как правильно выполнить отображение.
Обновление 2015-06-14
Успешно применил эту технику к огромной производственной базе данных с более чем 40 нормализованными таблицами. Он отлично работал, чтобы отобразить расширенный запрос SQL с более чем 16 inner join
и left join
в правильную иерархию POCO (с 4 уровнями вложенности). Запросы являются невероятно быстрыми, почти такими же быстрыми, как и ручное кодирование в ADO.NET (обычно это было 52 миллисекунды для запроса и 50 миллисекунд для отображения из плоского результата в иерархию POCO). В этом нет ничего революционного, но он наверняка превосходит Entity Framework по скорости и простоте использования, особенно если все, что мы делаем, это выполняем запросы.
Обновление 2016-02-19
Код работает безупречно в течение 9 месяцев. В последней версии Slapper.Automapper
есть все изменения, которые я применил для исправления проблемы, связанной с возвращением нулей в запросе SQL.
Обновление 2017-02-20
Код работает безупречно в течение 21 месяца и обрабатывает непрерывные запросы от сотен пользователей в компании FTSE 250.
Slapper.Automapper
также отлично подходит для отображения файла .csv прямо в список POCO. Считайте файл .csv в список IDictionary, затем сопоставьте его прямо с целевым списком POCO. Единственная хитрость заключается в том, что вам нужно добавить свойство int Id {get; set}
и убедиться, что он уникален для каждой строки (иначе автомат не сможет различить строки).
Обновление 2019-01-29
Незначительное обновление для добавления комментариев к коду.
См .: https://github.com/SlapperAutoMapper/Slapper.AutoMapper