Как мне написать один ко многим запрос в Dapper.Net? - PullRequest
70 голосов
/ 19 февраля 2012

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

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

Кто-нибудь может заметить ошибку?

EDIT:

Это мои сущности:

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

EDIT:

Я изменяю запрос на:

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

Ответы [ 6 ]

142 голосов
/ 06 мая 2015

В этом посте показано, как запросить сильно нормализованную базу данных 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

enter image description here

Таблица SQL TestPhone

Обратите внимание, что эта таблица имеет внешний ключ ContactID, который ссылается на таблицу TestContact (это соответствует List<TestPhone> в POCO выше).

enter image description here

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

enter image description here

код 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);   
            }
        }
    }
}

выход

enter image description here

POCO Entity Hierarchy

Глядя в Visual Studio, мы видим, что Slapper.Automapper правильно заполнил наши POCO-сущности, то есть у нас есть List<TestContact>, а у каждого TestContact есть List<TestPhone>.

enter image description here

Примечания

И 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

14 голосов
/ 24 октября 2015

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

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 запроса вместо одного, второй запрос используетВНУТРЕННЕЕ соединение вместо менее оптимального ВЛЕВОГО соединения.

7 голосов
/ 15 марта 2014

Небольшое изменение ответа Эндрю, в котором используется функция выбора родительского ключа вместо 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)
7 голосов
/ 19 февраля 2012

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

2 голосов
/ 30 июля 2012

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

    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();
    }
1 голос
/ 20 сентября 2018

Вот еще один метод:

Order (один) - 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();
}

Source : http://dapper -tutorial.net/ результат-мульти-отображение # пример --- запрос-мульти-карт-один-ко-многим

...