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


Связанный вопрос: 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();

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;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.