Як написати один до багатьох запитів у 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>у першому запиті.


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. Коду було настільки мало, що не було несподіваної поведінки.

Недоліки

  • Повернено масштаб понад 1000000 рядків. Добре працює при поверненні <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,463 мілісекунд на запит, використовуючи додатковий 3 lines of code for the query + mapping from dynamic to POCO Entities.

Працював приклад

У цьому прикладі ми маємо список Contacts, і кожен Contactможе мати один або кілька phone numbers.

Суб’єкти POCO

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-сутності, тобто ми маємо a List<TestContact>, і кожен TestContactмає a List<TestPhone>.

введіть тут опис зображення

Примітки

Як Dapper, так і Slapper. Automapper кешує все внутрішньо для швидкості. Якщо у вас трапляються проблеми з пам'яттю (дуже малоймовірно), переконайтеся, що час від часу очищаєте кеш для обох.

Переконайтесь, що ви називаєте стовпці, що повертаються, використовуючи нотацію підкреслення ( _), щоб надати Slapper.Automapper підказки про те, як відобразити результат в POCO Entities.

Переконайтеся, що ви даєте підказки Slapper.Automapper на первинному ключі для кожної сутності POCO (див. Рядки Slapper.AutoMapper.Configuration.AddIdentifiers). Ви також можете використовувати Attributesдля цього на POCO. Якщо ви пропустите цей крок, він може піти не так (теоретично), як Slapper. Automapper не знатиме, як правильно виконати відображення.

Оновлення 14.06.2015

Успішно застосували цю техніку до величезної виробничої бази даних із понад 40 нормалізованими таблицями. Це чудово працювало, щоб відобразити розширений SQL-запит із понад 16 inner joinі left joinдо відповідної ієрархії POCO (з 4 рівнями вкладеності). Запити сліпуче швидкі, майже такі ж швидкі, як і ручне кодування в ADO.NET (зазвичай це було 52 мілісекунди для запиту та 50 мілісекунд для відображення з плоского результату в ієрархію POCO). Це насправді нічого революційного, але це, безсумнівно, перевершує Entity Framework за швидкістю та простотою використання, особливо якщо все, що ми робимо, це запуски запитів.

Оновлення 2016-02-19

Code працює бездоганно у виробництві вже 9 місяців. Остання версія Slapper.Automapperмістить усі зміни, які я застосував, щоб вирішити проблему, пов’язану з поверненням нулів у запиті SQL.

Оновлення 2017-02-20

Code працює бездоганно у виробництві вже 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, хоча він не підтримує щось на зразок Dapper "splitOn"?
tbone

3
Це узгодження імені таблиці потрібне Slapper.Automapper. Так, Dapper дійсно підтримує відображення прямо в POCO, але я віддаю перевагу використанню Slapper.Automapper, оскільки код настільки чистий і ремонтопридатний.
Контанго,

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


5
Мені подобається такий підхід. Pure dapper та IMHO більш зрозуміле відображення.
Авнер

1
Здається, це було б легко ввести в метод розширення, який займає пару albmdas, одну для селектора ключів і одну для дочірнього селектора. Подібно до, .Join(але створює графік об’єкта замість згладженого результату.
AaronLS

8

Невелика модифікація відповіді Ендрю, яка використовує функцію для вибору батьківського ключа замість 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>(); } }
Клей

1
Це рішення є чудовим і працює для нас. Мені довелося додати в чек із children.add, щоб перевірити наявність null на випадок, якщо не повертаються дочірні рядки.
tlbignerd

7

Згідно з цією відповіддю, у Dapper.Net немає вбудованої підтримки для картографування. Запити завжди повертають по одному об’єкту на рядок бази даних. Однак є альтернативне рішення.


Мені шкода, але я не розумію, як я можу це використовувати у своєму запиті? Він намагається здійснити запит до бази даних 2 рази без об’єднань (і, наприклад, використовуючи жорстко закодований 1). У прикладі повертається лише 1 основна сутність, яка, у свою чергу, містить дочірні сутності. У моєму випадку я хочу спроектувати приєднання (список, який внутрішньо містить список). Як це зробити за посиланням, яке ви згадали? У посиланні, де в рядку написано: (contact, phones) => { contact.Phones = phones; } я повинен був би написати фільтр для телефонів, чиї контакти збігаються з контактами контактів. Це досить неефективно.
TCM

@Anthony Погляньте на відповідь Майка. Він виконує один запит з двома наборами результатів і приєднує їх згодом за допомогою методу Map. Звичайно, вам не потрібно твердо кодувати значення у вашому випадку. Я спробую скласти приклад за пару годин.
Дамір Арх

1
Гаразд, я нарешті запрацював Дякую! Не знаю, як це може вплинути на ефективність запитів бази даних у 2 рази, ніж можна було б здійснити за допомогою одного об’єднання.
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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.