Определить сущности, которые имеют одинаковых детей - PullRequest
4 голосов
/ 16 октября 2011

У меня есть две сущности, Class и Student, связанные во взаимосвязи "многие ко многим".

Когда данные импортируются из внешнего приложения, к сожалению, некоторые классы создаются в двух экземплярах.У «повторяющихся» классов разные имена, но один и тот же предмет и одни и те же учащиеся.

Например:

{Id = 341, Title = '10rs / PE1a', SubjectId= 60, студенты = {Джек, Билл, Сара}}

{Id = 429, название = '10rs / PE1b', SubjectId = 60, студенты = {Джек, Билл, Сара}}

Не существует общего правила сопоставления имен этих повторяющихся классов, поэтому единственный способ определить, что два класса являются дубликатами, - это иметь одинаковые SubjectId и Студенты .

Я бы хотел использовать LINQ для обнаружения всех дубликатов (и в конечном итоге их объединения).До сих пор я пытался:

var sb = new StringBuilder();
using (var ctx = new Ctx()) {
  ctx.CommandTimeout = 10000; // Because the next line takes so long!
  var allClasses = ctx.Classes.Include("Students").OrderBy(o => o.Id);
  foreach (var c in allClasses) {
    var duplicates = allClasses.Where(o => o.SubjectId == c.SubjectId && o.Id != c.Id && o.Students.Equals(c.Students));
    foreach (var d in duplicates)
      sb.Append(d.LongName).Append(" is a duplicate of ").Append(c.LongName).Append("<br />");
  }
}
lblResult.Text = sb.ToString();

Это не хорошо, потому что я получаю ошибку:

NotSupportedException : невозможно создать постоянное значение типа'TeachEDM.Student.В этом контексте поддерживаются только примитивные типы (такие как Int32, String и Guid).

Очевидно, мне не нравится, когда я пытаюсь сопоставить o.SubjectId == c.SubjectId в LINQ.

Кроме того, это кажется ужасным методом в целом и очень медленным.Вызов в базу данных занимает более 5 минут.

Буду очень признателен за совет.

1 Ответ

4 голосов
/ 16 октября 2011

Сравнение SubjectId не является проблемой, поскольку c.SubjectId является значением примитивного типа (я думаю, int).Исключение жалуется на Equals(c.Students).c.Students является константой (относительно запроса duplicates), но не является примитивным типом.

Я бы также попытался провести сравнение в памяти, а не в базе данных.В любом случае вы загружаете все данные в память, когда запускаете свой первый цикл foreach: он выполняет запрос allClasses.Затем внутри цикла вы расширяете IQueryable allClasses до IQueryable duplicates, который затем выполняется во внутреннем цикле foreach.Это один запрос к базе данных на элемент вашего внешнего цикла!Это может объяснить низкую производительность кода.

Поэтому я бы попытался выполнить содержимое первого foreach в памяти.Для сравнения списка Students необходимо сравнивать элемент за элементом, а не ссылки на коллекции учеников, поскольку они наверняка различны.

var sb = new StringBuilder();
using (var ctx = new Ctx())
{
    ctx.CommandTimeout = 10000; // Perhaps not necessary anymore
    var allClasses = ctx.Classes.Include("Students").OrderBy(o => o.Id)
        .ToList(); // executes query, allClasses is now a List, not an IQueryable

    // everything from here runs in memory
    foreach (var c in allClasses)
    {
        var duplicates = allClasses.Where(
           o => o.SubjectId == c.SubjectId &&
           o.Id != c.Id &&
           o.Students.OrderBy(s => s.Name).Select(s => s.Name)
            .SequenceEqual(c.Students.OrderBy(s => s.Name).Select(s => s.Name)));

        // duplicates is an IEnumerable, not an IQueryable
        foreach (var d in duplicates)
            sb.Append(d.LongName)
              .Append(" is a duplicate of ")
              .Append(c.LongName)
              .Append("<br />");
    }
}
lblResult.Text = sb.ToString();

Упорядочение последовательностей по имени необходимо, потому что,Я полагаю, SequenceEqual сравнивает длину последовательности, а затем элемент 0 с элементом 0, затем элемент 1 с элементом 1 и т. Д.

Редактировать На ваш комментарий, что первый запрос все еще медленный.

Если у вас 1300 классов с 30 учениками в каждом, производительность стремительной загрузки (Include) может пострадать отумножение данных, которые передаются между базой данных и клиентом.Это объясняется здесь: Сколько Включить я могу использовать для ObjectSet в EntityFramework для сохранения производительности? .Запрос является сложным, поскольку ему требуется JOIN между классами и учащимися, а материализация объекта также сложна, поскольку EF должен отфильтровывать дублированные данные при создании объектов.

Альтернативный подход заключается в загрузке толькоклассы без учеников в первом запросе, а затем явно загружать учеников один за другим внутри цикла.Это выглядело бы так:

var sb = new StringBuilder();
using (var ctx = new Ctx())
{
    ctx.CommandTimeout = 10000; // Perhaps not necessary anymore
    var allClasses = ctx.Classes.OrderBy(o => o.Id).ToList(); // <- No Include!
    foreach (var c in allClasses)
    {
        // "Explicite loading": This is a new roundtrip to the DB
        ctx.LoadProperty(c, "Students");
    }

    foreach (var c in allClasses)
    {
        // ... same code as above
    }
}
lblResult.Text = sb.ToString();

В этом примере вы бы имели 1 + 1300 запросов к базе данных вместо одного, но у вас не будет умножения данных, которое происходит при активной загрузке, а запросы проще(нет JOIN между классами и учениками).

Подробная загрузка объясняется здесь:

Если вы работаете с LazyЗагрузка первой foreach с помощью LoadProperty не потребуется, поскольку коллекции Students будут загружены при первом обращении к ней.Это должно привести к тем же 1300 дополнительным запросам, как явная загрузка.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...