Як зіставити списки вкладених об'єктів за допомогою Dapper


127

Наразі я використовую Entity Framework для доступу до db, але хочу ознайомитися з 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 робить для мене відображення, тому мій об'єкт курсу заповнений списком місць. Як би я поступив з цим разом із 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
Сем, у ~ великій програмі, де колекції регулярно експонуються на об’єктах домену, як у прикладі, де ви б рекомендували фізично розміщувати цей код ? (Припускаючи, що ви хочете споживати аналогічно повністю сконструйований [Курс] сутність з безлічі різних місць у вашому коді) У конструкторі? На класовій фабриці? Десь в іншому місці?
tbone

177

Крім того, ви можете використовувати один запит із пошуком:

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
У режимі ЛІВОГО ПРИЄДНАННЯ ви отримаєте нульовий елемент у списку місцеположень. Видаліть їх за допомогою елементів var = = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Чоко Сміт

Я не можу скласти це, якщо я не маю крапку з комою в кінці рядка 1 і не видаляю кому перед 'AsQueryable ()'. Я би відредагував відповідь, але 62 учасників раніше мені здалося, що це нормально, можливо, мені щось не вистачає ...
bitcoder

1
ДЛЯ ПРИЄДНАННЯ ВЛЕВО: Не потрібно робити ще один Прогноз щодо цього. Просто перевірте, перш ніж додавати його: if (l! = Null) course.Locations.Add (l).
jpgrassi

1
Оскільки ви використовуєте словник. Чи буде це швидше, якщо ви використовували QueryMultiple та запитуваний курс та місце розташування окремо, а потім використовували той самий словник для призначення місця для курсу? По суті те саме, мінус внутрішнє з'єднання, що означає, що sql не буде передавати стільки байтів?
MIKE

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

2
Єдина проблема з цим полягає в тому, що ви будете дублювати заголовок на кожній записи про місцезнаходження. Якщо на курсах є багато локацій, це може бути значна кількість дублювання даних, що буде проходити по дроту, що збільшить пропускну здатність, займе більше часу для розбору / карти та використання більше пам'яті, щоб прочитати все це.
Даніель Лоренц

10
Я не впевнений, що це працює так, як я очікував. У мене є 1 батьківський об'єкт з 3 пов’язаними об’єктами. запит, який я використовую, отримує три рядки назад. перші стовпці з описом батьків, які дублюються для кожного рядка; розділення на ідентифікатор визначить кожну унікальну дитину. мої результати - 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. Велика різниця у великих наборах даних.
Даніель Лоренц

6

Вибачте, що запізнилися на вечірку (як завжди). Для мене простіше використовувати модель 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.