Как написать запрос "один ко многим" в Dapper.Net?

80

Я написал этот код для проецирования отношения один ко многим, но он не работает:

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>в первом запросе.

TCM
источник
1
Как выглядят ваши сущности?
gideon
2
Как не работает? Вы получаете исключение? Неожиданные результаты?
driis
1
Ошибка не имеет смысла, поэтому я не удосужился ее опубликовать. Я получаю: "{" Значение не может быть нулевым. \ R \ nИмя параметра: con "}". Строка, которая вызывает ошибку в SqlMapper: "il.Emit (OpCodes.Newobj, type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));"
TCM

Ответы:

162

В этом посте показано, как запросить сильно нормализованную базу данных 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 обрабатывает произвольно вложенные иерархии, он не ограничен парой уровней вложенности. Мы легко можем внести быстрые изменения, и все по-прежнему будет работать.
  • Отладка . Сначала мы можем увидеть, что SQL-запрос работает правильно, а затем мы можем проверить, что результат SQL-запроса правильно сопоставлен с целевыми объектами POCO.
  • Легкость разработки на SQL . Я считаю, что создание плоских запросов с inner joinsвозвращением плоских результатов намного проще, чем создание нескольких операторов выбора с сшиванием на стороне клиента.
  • Оптимизированные запросы в SQL . В сильно нормализованной базе данных создание плоского запроса позволяет механизму SQL применять расширенную оптимизацию ко всему, что обычно было бы невозможно, если бы было построено и запущено много небольших индивидуальных запросов.
  • Доверие . Dapper - это серверная часть StackOverflow, а Рэнди Бёрден - своего рода суперзвезда. Мне нужно что-то еще сказать?
  • Скорость развития. Я смог выполнить несколько чрезвычайно сложных запросов с множеством уровней вложенности, и время разработки было довольно низким.
  • Меньше ошибок. Я написал это однажды, он просто работал, и теперь этот метод помогает компании FTSE. Кода было так мало, что не было неожиданного поведения.

Недостатки

  • Возвращено масштабирование за пределы 1 000 000 строк. Хорошо работает при возврате <100 000 строк. Однако, если мы возвращаем> 1000000 строк, чтобы уменьшить трафик между нами и 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,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

Контанго
источник
1
Мне действительно не нравится соглашение о префиксе имени таблицы во всех ваших sql, хотя оно не поддерживает что-то вроде «splitOn» Dapper?
tbone
3
Это соглашение об именах таблиц требуется для Slapper.Automapper. Да, в Dapper есть поддержка прямого сопоставления с POCO, но я предпочитаю использовать Slapper.Automapper, так как код очень чистый и удобный.
Contango,
2
Думаю, я бы использовал Slapper, если бы вам не нужно было использовать псевдонимы для всех столбцов - вместо этого в вашем примере я хотел бы иметь возможность сказать:, splitOn: «PhoneId» - разве это не было бы совсем немного проще, чем все называть псевдонимом?
tbone
1
Мне очень нравится вид шлепка, просто интересно, пробовали ли вы левое соединение, когда у человека нет контактных номеров? Есть ли у вас хороший способ справиться с этим?
Не любил
1
@tbone splitOn не содержит никакой информации о том, где в вашем объекте принадлежит этот элемент, поэтому slapper использует такой путь
не любил
20

Я хотел, чтобы это было как можно проще, мое решение:

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.

Дэви
источник
5
Мне нравится такой подход. Чистый даппер и ИМХО более понятный маппинг.
Avner
1
Похоже, это было бы легко поместить в метод расширения, который принимает пару albmdas, один для селектора ключей и один для дочернего селектора. Аналогично, .Join(но создает граф объекта вместо плоского результата.
AaronLS
8

Небольшая модификация ответа Эндрю, которая использует 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>(); } }
Clay,
1
Это отличное решение, которое у нас сработало. Мне пришлось добавить проверку с помощью children.add, чтобы проверить значение null на случай, если не было возвращенных дочерних строк.
tlbignerd 07
7

Согласно этому ответу, в Dapper.Net нет встроенной поддержки отображения "один-ко-многим". Запросы всегда возвращают один объект для каждой строки базы данных. Однако есть альтернативное решение.

Дамир Арх
источник
Извините, но я не понимаю, как использовать это в моем запросе? Он пытается запросить базу данных 2 раза без объединений (в примере с жестко запрограммированной 1). В примере возвращается только 1 основная сущность, которая, в свою очередь, содержит дочерние сущности. В моем случае я хочу присоединиться к проекту (список, который внутри содержит список). Как мне это сделать с указанной вами ссылкой? В ссылке, где написано: (contact, phones) => { contact.Phones = phones; } мне нужно было бы написать фильтр для телефонов, у которых contactid совпадает с contactid контакта. Это довольно неэффективно.
TCM
@Anthony Взгляните на ответ Майка. Он выполняет один запрос с двумя наборами результатов и затем объединяет их с помощью метода Map. Конечно, вам не нужно жестко кодировать значение в вашем случае. Попробую через пару часов собрать пример.
Damir Arh
1
хорошо, я наконец-то заработал. Благодаря! Не знаю, как это повлияет на производительность запросов к базе данных в два раза больше, чем можно было бы выполнить с помощью одного соединения.
TCM
2
Также я не понимаю, какие изменения мне нужно было бы внести, если бы было 3 стола: p
TCM
1
это полный отстой .. зачем избегать присоединений?
GorillaApe
2

Вот грубый обходной путь

    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();
    }
Эндрю Баллок
источник
11
Реализация кеша некорректна. Хеш-коды не уникальны - два объекта могут иметь один и тот же хеш-код. Это может привести к тому, что список объектов будет заполнен элементами, принадлежащими другому объекту ..
stmax
2

Вот еще один способ:

Заказ (один) - 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

Exocomp
источник