LINQ To SQL в параллельном цикле: как предотвратить повторяющиеся вставки?

У меня возникают проблемы при попытке распараллелить дорогостоящую интеграцию API.

Интеграция параллельно запрашивает API и заполняет коллекцию ConcurrentBag. Выполняется некоторая обработка, а затем она передается в Parallel.ForEach(), в котором она взаимодействует с базой данных с помощью LINQ To Sql.

Есть:

  1. один внешний цикл, который работает параллельно для курсов

  2. внутренний цикл через дисциплины

  3. внутри него еще один цикл, повторяющий уроки.

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

В настоящее время код выглядит так:

(externalCourseList – это коллекция типа ConcurrentBag<ExternalCourse>.)

Parallel.ForEach(externalCourseList, externalCourse =>
{
    using ( var context = new DataClassesDataContext() )
    {
        var dbCourse = context.Courses.Single( x => x.externalCourseId == externalCourse.courseCode.ToString() );

        dbCourse.ShortDesc = externalCourse.Summary;
        //dbCourse.LongDesc = externalCourse.MoreInfo;
        //(etc)

        foreach (var externalDiscipline in externalCourse.Disciplines)
        {
            var dbDiscipline = context.Disciplines.Where(
                x => x.ExternalDisciplineID == externalDiscipline.DisciplineCode.ToString() ).SingleOrDefault();

            if (dbDiscipline == null)
                dbDiscipline = new Linq2SQLEntities.Discipline();

            dbDiscipline.Title = externalDiscipline.Name;
            //(etc)
            dbDiscipline.ExternalDisciplineID = externalDiscipline.DisciplineCode.ToString();

            if (!dbDiscipline.IsLoaded)
                context.Disciplines.InsertOnSubmit(dbDiscipline);

            // relational table used as one-to-many relationship for legacy reasons
            var courseDiscipline = dbDiscipline.Course_Disciplines.SingleOrDefault(x => x.CourseID == dbCourse.CourseID);

            if (courseDiscipline == null)
            {
                courseDiscipline = new Course_Discipline
                {
                    Course = dbCourse,
                    Discipline = dbDiscipline
                };

                context.Course_Disciplines.InsertOnSubmit(courseDiscipline);
            }

            foreach (var externalLesson in externalDiscipline.Lessons)
            {
                /// The next statement throws an exception
                var dbLesson = context.Lessons.Where(
                    x => x.externalLessonID == externalLesson.LessonCode).SingleOrDefault();

                if (dbLesson == null)
                    dbLesson = new Linq2SQLEntities.Lesson();

                dbLesson.Title = externalLesson.Title;
                //(etc)
                dbLesson.externalLessonID = externalLesson.LessonCode;

                if (!dbLesson.IsLoaded)
                    context.Lessons.InsertOnSubmit(dbLesson);

                var disciplineLesson = dbLesson.Discipline_Lessons.SingleOrDefault(
                    x => x.DisciplineID == dbDiscipline.DisciplineID && x.LessonID == dbLesson.LessonID);

                if (disciplineLesson == null)
                {
                    disciplineLesson = new Discipline_Lesson
                    {
                        Discipline = dbDiscipline,
                        Lesson = dbLesson
                    };

                    context.Discipline_Lessons.InsertOnSubmit(disciplineLesson);
                }
            }

        }

        context.SubmitChanges();
    }
}
);

(IsLoaded реализовано, как описано здесь.)

Исключение создается для строки, которой предшествует ///, поскольку один и тот же урок часто вставляется несколько раз, а вызов .SingleOrDefault() на context.Lessons.Where(x => x.externalLessonID == externalLesson.LessonCode) завершается ошибкой.

Что было бы лучшим способом решить эту проблему?

Спасибо.


person Marc.2377    schedule 13.12.2018    source источник
comment
Просто идея: вместо того, чтобы давать весь список, вызовите GroupBy() и попытайтесь создать блоки, которые не конфликтуют в нужной точке. Другая возможность (которую я никогда не использовал, поэтому не знаю, работает ли она) — использовать Partitioner. Parallel.ForEach принимает его как дополнительный параметр и также помогает сгруппировать данные осмысленным образом для распараллеливания.   -  person Oliver    schedule 13.12.2018
comment
Спасибо @Oliver, пока я следую вашей идее.   -  person Marc.2377    schedule 14.12.2018