Я написал этот код для проецирования отношения один ко многим, но он не работает:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
(@"Select Stores.Id as StoreId, Stores.Name,
Employees.Id as EmployeeId, Employees.FirstName,
Employees.LastName, Employees.StoreId
from Store Stores
INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; },
splitOn: "EmployeeId");
foreach (var store in stores)
{
Console.WriteLine(store.Name);
}
}
Кто-нибудь может заметить ошибку?
РЕДАКТИРОВАТЬ:
Это мои сущности:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public IList<Store> Stores { get; set; }
public Product()
{
Stores = new List<Store>();
}
}
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Employee> Employees { get; set; }
public Store()
{
Products = new List<Product>();
Employees = new List<Employee>();
}
}
РЕДАКТИРОВАТЬ:
Я меняю запрос на:
IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
(@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
Employees.FirstName,Employees.LastName,Employees.StoreId
from Store Stores INNER JOIN Employee Employees
ON Stores.Id = Employees.StoreId",
(a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");
и избавляюсь от исключений! Однако сотрудники вообще не отображаются. Я до сих пор не уверен, с чем возникла проблема IEnumerable<Employee>
в первом запросе.
Ответы:
В этом посте показано, как запросить сильно нормализованную базу данных SQL и сопоставить результат с набором сильно вложенных объектов C # POCO.
Ингредиенты:
Понимание, которое позволило мне решить эту проблему, - отделить
MicroORM
отmapping the result back to the POCO Entities
. Таким образом, мы используем две отдельные библиотеки:По сути, мы используем Dapper для запроса к базе данных, а затем используем Slapper.Automapper для отображения результата прямо в наши POCO.
Преимущества
List<MyClass1>
который, в свою очередь, содержитList<MySubClass2>
, и т. Д.).inner joins
возвращением плоских результатов намного проще, чем создание нескольких операторов выбора с сшиванием на стороне клиента.Недостатки
inner join
(что возвращает дубликаты), вместо этого мы должны использовать несколькоselect
операторов и объединить все вместе на на стороне клиента (см. другие ответы на этой странице).Тестирование производительности
В моих тестах 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,463 миллисекунды на запрос, используя дополнительный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
Глядя в Visual Studio, мы видим, что Slapper.Automapper правильно заполнил наши объекты POCO, т.е. у нас есть
List<TestContact>
и у каждогоTestContact
естьList<TestPhone>
.Ноты
И Dapper, и Slapper.Automapper кешируют все внутренне для скорости. Если вы столкнетесь с проблемами памяти (что очень маловероятно), убедитесь, что вы время от времени очищаете кеш для них обоих.
Убедитесь, что вы назвали возвращаемые столбцы, используя нотацию подчеркивания (
_
), чтобы дать Slapper.Automapper подсказки о том, как отобразить результат в объекты POCO.Убедитесь, что вы даете Slapper.Automapper подсказки о первичном ключе для каждой POCO Entity (см. Строки
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
есть все изменения, которые я применил для устранения проблемы, связанной с возвращением значений NULL в запросе 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
источник
Я хотел, чтобы это было как можно проще, мое решение:
public List<ForumMessage> GetForumMessagesByParentId(int parentId) { var sql = @" select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key] from t_data d where d.cd_data = @DataId order by id_data asc; select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal from t_data d inner join T_data_image di on d.id_data = di.cd_data inner join T_image i on di.cd_image = i.id_image where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;"; var mapper = _conn.QueryMultiple(sql, new { DataId = parentId }); var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v); var images = mapper.Read<ForumMessageImage>().ToList(); foreach(var imageGroup in images.GroupBy(g => g.DataId)) { messages[imageGroup.Key].Images = imageGroup.ToList(); } return messages.Values.ToList(); }
Я все еще делаю один вызов базы данных, и хотя теперь я выполняю 2 запроса вместо одного, второй запрос использует соединение INNER вместо менее оптимального соединения LEFT.
источник
.Join(
но создает граф объекта вместо плоского результата.Небольшая модификация ответа Эндрю, которая использует Func для выбора родительского ключа вместо
GetHashCode
.public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>( this IDbConnection connection, string sql, Func<TParent, TParentKey> parentKeySelector, Func<TParent, IList<TChild>> childSelector, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>(); connection.Query<TParent, TChild, TParent>( sql, (parent, child) => { if (!cache.ContainsKey(parentKeySelector(parent))) { cache.Add(parentKeySelector(parent), parent); } TParent cachedParent = cache[parentKeySelector(parent)]; IList<TChild> children = childSelector(cachedParent); children.Add(child); return cachedParent; }, param as object, transaction, buffered, splitOn, commandTimeout, commandType); return cache.Values; }
Пример использования
conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
источник
class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Согласно этому ответу, в Dapper.Net нет встроенной поддержки отображения "один-ко-многим". Запросы всегда возвращают один объект для каждой строки базы данных. Однако есть альтернативное решение.
источник
(contact, phones) => { contact.Phones = phones; }
мне нужно было бы написать фильтр для телефонов, у которых contactid совпадает с contactid контакта. Это довольно неэффективно.Вот грубый обходной путь
public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { var cache = new Dictionary<int, TOne>(); cnn.Query<TOne, TMany, TOne>(sql, (one, many) => { if (!cache.ContainsKey(one.GetHashCode())) cache.Add(one.GetHashCode(), one); var localOne = cache[one.GetHashCode()]; var list = property(localOne); list.Add(many); return localOne; }, param as object, transaction, buffered, splitOn, commandTimeout, commandType); return cache.Values; }
это ни в коем случае не самый эффективный способ, но он поможет вам начать работу. Я постараюсь оптимизировать это, когда у меня будет возможность.
используйте это так:
conn.Query<Product, Store>("sql here", prod => prod.Stores);
имейте в виду, что ваши объекты необходимо реализовать
GetHashCode
, возможно, так:public override int GetHashCode() { return this.Id.GetHashCode(); }
источник
Вот еще один способ:
Заказ (один) - OrderDetail (много)
using (var connection = new SqlCeConnection(connectionString)) { var orderDictionary = new Dictionary<int, Order>(); var list = connection.Query<Order, OrderDetail, Order>( sql, (order, orderDetail) => { Order orderEntry; if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry)) { orderEntry = order; orderEntry.OrderDetails = new List<OrderDetail>(); orderDictionary.Add(orderEntry.OrderID, orderEntry); } orderEntry.OrderDetails.Add(orderDetail); return orderEntry; }, splitOn: "OrderDetailID") .Distinct() .ToList(); }
Источник : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many
источник