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

127

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

b3n
источник
Связанный вопрос: stackoverflow.com/questions/6379155/…
Jeroen K
вот мое решение: stackoverflow.com/a/57395072/8526957
Sam Sch

Ответы:

57

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

Сэм Шафран
источник
Спасибо за разъяснение Сэм. Как вы описали выше, я просто выполняю второй запрос, выбирая местоположения и вручную назначаю их курсу. Я просто хотел убедиться, что не пропустил что-то, что позволило бы мне сделать это с помощью одного запроса.
b3n
2
Сэм, в большом приложении, где коллекции регулярно отображаются в объектах домена, как в примере, где вы порекомендуете физически размещать этот код ? (Предполагая, что вы хотите использовать полностью сконструированную подобную сущность [Course] из множества различных мест в вашем коде) В конструкторе? На фабрике классов? Где-нибудь еще?
tbone
178

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

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/

Йерун К.
источник
9
Это сэкономило мне массу времени. Мне потребовалась одна модификация, которая может понадобиться другим, - это включить аргумент splitOn :, поскольку я не использовал стандартный «Id».
Билл Самброн
1
Для LEFT JOIN вы получите нулевой элемент в списке местоположений. Удалите их с помощью var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
Я не могу скомпилировать это, если у меня нет точки с запятой в конце строки 1 и не удалю запятую перед «AsQueryable ()». Я бы отредактировал ответ, но 62 проголосовавших до меня, казалось, думали, что все в порядке, может, я что-то
упускаю
1
Для LEFT JOIN: не нужно делать для него еще один Foreach. Просто проверьте перед добавлением: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Поскольку вы пользуетесь словарем. Было бы это быстрее, если бы вы использовали QueryMultiple и запрашивали курс и местоположение отдельно, а затем использовали тот же словарь для назначения местоположения для курса? По сути, это то же самое, за исключением внутреннего соединения, что означает, что sql не будет передавать столько байтов?
МАЙК
43

Нет необходимости в 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();
tchelidze
источник
3
Это отлично - на мой взгляд, это должен быть выбранный ответ. Тем не менее, люди, которые делают это, должны быть осторожны, поскольку это может повлиять на производительность.
cr1pto 01
2
Единственная проблема в том, что вы будете дублировать заголовок в каждой записи Location. Если на курс много местоположений, это может быть значительный объем дублирования данных, проходящих по сети, что увеличит пропускную способность, займет больше времени на синтаксический анализ / сопоставление и будет использовать больше памяти для чтения всего этого.
Daniel Lorenz
10
Я не уверен, что это работает так, как я ожидал. У меня есть 1 родительский объект с 3 связанными объектами. запрос, который я использую, возвращает три строки назад. первые столбцы, описывающие родителя, дублируются для каждой строки; разделение на id идентифицирует каждого уникального потомка. мои результаты - 3 одинаковых родителя с 3 детьми .... должен быть один родитель с 3 детьми.
topwik
2
@topwik прав. у меня тоже не работает так, как ожидалось.
Maciej Pszczolinski
3
На самом деле у меня было 3 родителя, по 1 ребенку в каждом с этим кодом. Не уверен, почему мой результат отличается от результата @topwik, но все же он не работает.
th3morg
29

Я знаю, что очень опаздываю, но есть другой вариант. Здесь вы можете использовать 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();
}
Даниэль Лоренц
источник
3
Одно замечание. Если локаций / курсов много, вы должны прокрутить локации один раз и поместить их в поиск по словарю, чтобы у вас было N log N вместо N ^ 2 скорости. Имеет большое значение для больших наборов данных.
Daniel Lorenz
6

Извините за опоздание на вечеринку (как всегда). Для меня проще использовать a 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;
}
Франсиско Тена
источник
4

Что-то пропало. Если вы не укажете каждое поле из 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.*в запросе, у меня был список локаций, но без данных.

Эдуардо Пирес
источник
0

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

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;
Kiichi
источник