Как сопоставить списки вложенных объектов с помощью Dapper

В настоящее время я использую Entity Framework для доступа к базе данных, но хочу взглянуть на Dapper. У меня есть такие классы:

public class Course{
   public string Title{get;set;}
   public IList<Location> Locations {get;set;}
   ...
}

public class Location{
   public string Name {get;set;}
   ...
}

Таким образом, один курс можно преподавать в нескольких местах. Entity Framework выполняет сопоставление за меня, поэтому мой объект Course заполняется списком местоположений. Как мне сделать это с помощью Dapper, возможно ли это или мне нужно сделать это в несколько этапов запроса?


person b3n    schedule 22.09.2011    source источник
comment
Связанный вопрос: stackoverflow .com / questions / 6379155 /   -  person Jeroen K    schedule 19.07.2013
comment
вот мое решение: stackoverflow.com/a/57395072/8526957   -  person Sam Sch    schedule 07.08.2019


Ответы (8)


Dapper не является полноценным ORM, он не обрабатывает волшебную генерацию запросов и тому подобное.

Для вашего конкретного примера, вероятно, сработает следующее:

Возьмите курсы:

var courses = cnn.Query<Course>("select * from Courses where Category = 1 Order by CreationDate");

Возьмите соответствующее отображение:

var mappings = cnn.Query<CourseLocation>(
   "select * from CourseLocations where CourseId in @Ids", 
    new {Ids = courses.Select(c => c.Id).Distinct()});

Захватите соответствующие места

var locations = cnn.Query<Location>(
   "select * from Locations where Id in @Ids",
   new {Ids = mappings.Select(m => m.LocationId).Distinct()}
);

Сопоставьте все это

Предоставляя это читателю, вы создаете несколько карт и повторяете свои курсы, заполняя их местоположениями.

Предостережение, трюк с in будет работать, если у вас меньше 2100 поисков (Sql Server), если у вас их больше, вы, вероятно, захотите изменить запрос на select * from CourseLocations where CourseId in (select Id from Courses ... ), в этом случае вы можете также восстановить все результаты за один раз, используя QueryMultiple

person Sam Saffron    schedule 23.09.2011
comment
Спасибо за разъяснение, Сэм. Как вы описали выше, я просто выполняю второй запрос, выбирая местоположения и вручную назначаю их курсу. Я просто хотел убедиться, что не пропустил что-то, что позволило бы мне сделать это с помощью одного запроса. - person b3n; 23.09.2011
comment
Сэм, в ~ большом приложении, где коллекции регулярно отображаются в объектах домена, как в примере, где вы бы порекомендовали физически разместить этот код? (Предполагая, что вы хотите использовать полностью сконструированную подобным образом сущность [Course] из множества разных мест в вашем коде) В конструкторе? В классной фабрике? Где-нибудь еще? - person tbone; 27.08.2015

В качестве альтернативы вы можете использовать один запрос с поиском:

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course))
            lookup.Add(c.Id, course = c);
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(l); /* Add locations to course */
        return course;
     }).AsQueryable();
var resultList = lookup.Values;

См. Здесь https://www.tritac.com/blog/dappernet-by-example/

person Jeroen K    schedule 19.07.2013
comment
Это сэкономило мне массу времени. Одна необходимая мне модификация, которая может понадобиться другим, - это включение аргумента splitOn :, поскольку я не использовал идентификатор по умолчанию. - person Bill Sambrone; 07.01.2015
comment
Для LEFT JOIN вы получите нулевой элемент в списке местоположений. Удалите их с помощью var items = lookup.Values; items.ForEach (x = ›x.Locations.RemoveAll (y =› y == null)); - person Choco Smith; 23.01.2015
comment
Я не могу скомпилировать это, если у меня нет точки с запятой в конце строки 1 и не удалю запятую перед «AsQueryable ()». Я бы отредактировал ответ, но 62 проголосовавших до меня, казалось, думали, что все в порядке, может, я что-то упускаю ... - person bitcoder; 15.07.2016
comment
@BillSambrone Привет, Билл, у меня есть список string вместо списка Location, на что мне разделить? - person Quentin; 27.08.2016
comment
@BillSambrone Nvm. Я понял. Ваш комментарий был полезен. Спасибо! - person Quentin; 27.08.2016
comment
Для LEFT JOIN: не нужно делать для него еще один Foreach. Просто проверьте перед добавлением: if (l! = Null) course.Locations.Add (l). - person jpgrassi; 13.07.2017
comment
Поскольку вы пользуетесь словарем. Было бы это быстрее, если бы вы использовали QueryMultiple и запрашивали курс и местоположение отдельно, а затем использовали тот же словарь для назначения местоположения для курса? По сути, это то же самое, за исключением внутреннего соединения, что означает, что sql не будет передавать столько байтов? - person MIKE; 31.05.2018

Нет необходимости в lookup Словаре

var coursesWithLocations = 
    conn.Query<Course, Location, Course>(@"
        SELECT c.*, l.*
        FROM Course c
        INNER JOIN Location l ON c.LocationId = l.Id                    
        ", (course, location) => {
            course.Locations = course.Locations ?? new List<Location>();
            course.Locations.Add(location); 
            return course;
        }).AsQueryable();
person tchelidze    schedule 17.10.2017
comment
Это отлично - на мой взгляд, это должен быть выбранный ответ. Тем не менее, люди, делающие это, должны быть осторожны *, поскольку это может повлиять на производительность. - person cr1pto; 01.12.2017
comment
@ randomus1r, я не понимаю, что вы имеете в виду. Мое замечание предостерегает пользователей от выполнения SELECT * в целом, не говоря уже о том, что он неправильно использует SELECT *. - person cr1pto; 09.05.2018
comment
Единственная проблема в том, что вы будете дублировать заголовок в каждой записи Location. Если на курс много местоположений, это может быть значительный объем дублирования данных, проходящих по сети, что увеличит пропускную способность, займет больше времени на синтаксический анализ / сопоставление и будет использовать больше памяти для чтения всего этого. - person Daniel Lorenz; 20.08.2018
comment
@DanielLorenz - какая альтернатива? Множественные запросы? Вы бы заполнили запрос, чтобы получить местоположения для каждого курса? Или вы запускаете какую-то операцию, чтобы создать список идентификаторов курса и передать его в запрос местоположения для соединения? Я предпочитаю приведенное выше решение, чтобы загрузить плоский объект и вложить его по мере необходимости. Вот как Entity Framework также выполняет вложение объектов. - person TheCrimsonSpace; 28.11.2018
comment
@TheCrimsonSpace QueryMultiple, где второй запрос имеет еще 1 поле от родителя для его сопоставления. Затем мы дублируем 1 столбец вместо всего этого. Затем, когда мы отображаем его, мы используем словарь, чтобы получить n log n. Я ненавижу то, как EF дублирует все эти данные. это не причинило мне ничего, кроме горя из-за объема данных, с которыми я работаю, и теперь я вообще активно избегаю выполнения каких-либо запросов на получение через EF. - person Daniel Lorenz; 28.11.2018
comment
Я не уверен, что это работает так, как я ожидал. У меня есть 1 родительский объект с 3 связанными объектами. запрос, который я использую, возвращает три строки назад. первые столбцы, описывающие родителя, дублируются для каждой строки; разделение по идентификатору идентифицирует каждого уникального потомка. мои результаты - 3 одинаковых родителя с 3 детьми .... должен быть один родитель с 3 детьми. - person topwik; 11.12.2018
comment
@topwik прав. у меня тоже не работает так, как ожидалось. - person Maciej Pszczolinski; 04.03.2019
comment
На самом деле у меня было 3 родителя, по 1 ребенку в каждом с этим кодом. Не уверен, почему мой результат отличается от результата @topwik, но все же он не работает. - person th3morg; 15.03.2019
comment
теперь в C # 8.0 вы можете упростить присвоение: course.Locations ??= new List<Location>(); - person Majid; 14.11.2019
comment
Этот ответ неверен, потому что при возврате с одним курсом и 3 местоположениями в базе данных будет возвращено 3 курса, каждый с одним местоположением. - person Delphi.Boy; 08.05.2020

Я знаю, что очень поздно, но есть другой вариант. Здесь вы можете использовать QueryMultiple. Что-то вроде этого:

var results = cnn.QueryMultiple(@"
    SELECT * 
      FROM Courses 
     WHERE Category = 1 
  ORDER BY CreationDate
          ; 
    SELECT A.*
          ,B.CourseId 
      FROM Locations A 
INNER JOIN CourseLocations B 
        ON A.LocationId = B.LocationId 
INNER JOIN Course C 
        ON B.CourseId = B.CourseId 
       AND C.Category = 1
");

var courses = results.Read<Course>();
var locations = results.Read<Location>(); //(Location will have that extra CourseId on it for the next part)
foreach (var course in courses) {
   course.Locations = locations.Where(a => a.CourseId == course.CourseId).ToList();
}
person Daniel Lorenz    schedule 23.02.2017
comment
Одно замечание. Если локаций / курсов много, вы должны прокрутить локации один раз и поместить их в поиск по словарю, чтобы у вас было N log N вместо N ^ 2 скорости. Имеет большое значение для больших наборов данных. - person Daniel Lorenz; 15.12.2017

Извините за опоздание на вечеринку (как всегда). Для меня проще использовать Dictionary, как это сделал Jeroen K с точки зрения производительности и удобочитаемости. Кроме того, чтобы избежать умножения заголовков в местах, я использую Distinct() для удаления потенциальных дубликатов:

string query = @"SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id";
using (SqlConnection conn = DB.getConnection())
{
    conn.Open();
    var courseDictionary = new Dictionary<Guid, Course>();
    var list = conn.Query<Course, Location, Course>(
        query,
        (course, location) =>
        {
            if (!courseDictionary.TryGetValue(course.Id, out Course courseEntry))
            {
                courseEntry = course;
                courseEntry.Locations = courseEntry.Locations ?? new List<Location>();
                courseDictionary.Add(courseEntry.Id, courseEntry);
            }

            courseEntry.Locations.Add(location);
            return courseEntry;
        },
        splitOn: "Id")
    .Distinct()
    .ToList();

    return list;
}
person Francisco Tena    schedule 10.10.2018

Чего-то не хватает. Если вы не укажете каждое поле из Locations в запросе SQL, объект Location не может быть заполнен. Посмотри:

var lookup = new Dictionary<int, Course>()
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.Name, l.otherField, l.secondField
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course)) {
            lookup.Add(c.Id, course = c);
        }
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(a);
        return course;
     },
     ).AsQueryable();
var resultList = lookup.Values;

Используя l.* в запросе, у меня был список мест, но без данных.

person Eduardo Pires    schedule 14.02.2014

Не уверен, что это кому-то нужно, но у меня есть динамическая версия без модели для быстрого и гибкого кодирования.

var lookup = new Dictionary<int, dynamic>();
conn.Query<dynamic, dynamic, dynamic>(@"
    SELECT A.*, B.*
    FROM Client A
    INNER JOIN Instance B ON A.ClientID = B.ClientID                
    ", (A, B) => {
        // If dict has no key, allocate new obj
        // with another level of array
        if (!lookup.ContainsKey(A.ClientID)) {
            lookup[A.ClientID] = new {
                ClientID = A.ClientID,
                ClientName = A.Name,                                        
                Instances = new List<dynamic>()
            };
        }

        // Add each instance                                
        lookup[A.ClientID].Instances.Add(new {
            InstanceName = B.Name,
            BaseURL = B.BaseURL,
            WebAppPath = B.WebAppPath
        });

        return lookup[A.ClientID];
    }, splitOn: "ClientID,InstanceID").AsQueryable();

var resultList = lookup.Values;
return resultList;
person Kiichi    schedule 18.07.2017

Есть еще один подход, использующий результат JSON. Несмотря на то, что принятый и другие ответы хорошо объяснены, я просто подумал о другом подходе к получению результата.

Создайте хранимую процедуру или выберите qry, чтобы вернуть результат в формате json. затем десериализуйте объект результата в требуемый формат класса. просмотрите пример кода.

using (var db = connection.OpenConnection())
{                
  var results = await db.QueryAsync("your_sp_name",..);
  var result = results.FirstOrDefault();    
                    
  string Json = result?.your_result_json_row;
                   
  if (!string.IsNullOrEmpty(Json))
  {
     List<Course> Courses= JsonConvert.DeserializeObject<List<Course>>(Json);
  }
    
  //map to your custom class and dto then return the result        
}

Это еще один мыслительный процесс. Пожалуйста, просмотрите то же самое.

person AcAnanth    schedule 16.05.2021