Шаблон репозитория — мы используем его неправильно?

В последнее время шаблон репозитория получил широкую огласку — если вы не уверены, как он работает, я бы посоветовал вам взглянуть на великолепную серию веб-трансляций MVC Storefront Роба Коннери . При реализации шаблона репозитория пару недель назад меня поразило, что я могу вводить незначительные ошибки параллелизма.

Ленивый список

Проблема не связана с самим шаблоном репозитория — проблема связана с использованием класса Lazy List, также представленного в серии MVC Storefront. Класс Lazy List позволяет позднее загружать данные, связанные с родительским объектом, — это способ ленивой загрузки с шаблоном репозитория (отсюда и название).

Реализация очень проста — мы в основном оборачиваем запрос для загрузки связанных данных в реализацию IList и выполняем запрос только при необходимости. Вы можете найти реализацию и обсуждение того, как это произошло в блоге Роба Коннери .

Не поймите меня неправильно — в реализации нет абсолютно ничего плохого — я просто пытаюсь проиллюстрировать, что мы можем использовать этот класс таким образом, чтобы вводить ошибки параллелизма.

пример

Некоторое время назад я написал приложение, похожее на StackOverflow, в качестве иллюстрации MVC — я собираюсь использовать это в качестве примера. В этой модели предметной области у нас есть объект записи, а запись имеет ноль или более комментариев.

Это идеальный сценарий для использования класса Lazy List — мы хотим отложить загрузку комментариев до их использования. Альтернативой является загрузка всех комментариев, когда мы загружаем объект post — ненужные (и, возможно, дорогостоящие) накладные расходы.

public IQueryable<Domain.Post> GetPosts()
{
    var posts = from post in dataContext.Posts
                join user in dataContext.Users on post.CreatedBy equals user.Id
                select new Domain.Post
                {
                    Id = post.Id,
                    Title = post.Title,
                    Body = post.Body,
                    Rating = post.Rating,
                    Views = post.Views,
                    Tags = post.Tags,
                    CreatedOn = post.CreatedOn,
                    CreatedBy = user.Username,
                    Timestamp = post.Timestamp,
                    Comments = GetCommentsForPost(post.Id)
                };

    return posts;
}

private IList<Domain.Comment> GetCommentsForPost(int postId)
{
    var comments = from comment in dataContext.Comments
                   where comment.PostId == postId
                   select new Domain.Comment
                   {
                       Id = comment.Id,
                       Message = comment.Message,
                       CreatedOn = comment.CreatedOn,
                       Timestamp = comment.Timestamp
                   };

    return new LazyList<Domain.Comment>(comments);
}

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

Есть 2 сценария, которые я хочу осветить с точки зрения проблем параллелизма. Для начала давайте взглянем на один пост с 2 комментариями.

Итак, представьте, что разработчик Джо пришел и задал вопрос об Атрибутах в C #, и до сих пор было 2 комментария. Я загрузил сообщение, используя вышеуказанное хранилище, и запрос на загрузку связанных комментариев был загружен в ленивый объект списка, но еще не выполнен .

Загрузка данных, которых сначала не было

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

var post = repository.GetPosts().WithId(2);

AddComment(post, "Added after we loaded the post");

DisplayPost(post);

Ленивая загрузка работает, и в результате мы загружаем данные, которых не было, когда мы загружали исходное сообщение. Так что же должно было случиться? Какие данные мы должны были увидеть?

Ленивая загрузка привела к тому, что мы загрузили правильные данные . Если бы мы не лениво загружали комментарии (и загружали их заранее вместе с сообщением), мы бы пропустили последний комментарий — наш объект сообщения был бы устаревшим . Довольно круто — ленивая загрузка фактически привела к загрузке самых актуальныхсвязанных данных.

Теперь давайте посмотрим на другой сценарий, где результаты не так велики.

Загрузка данных, которых там быть не должно

Представьте себе тот же сценарий, что и выше, но вместо добавления простого комментария разработчик Джо изменил свой первоначальный пост, и 2 комментария были добавлены в связи с его изменением. </ P>

var post = repository.GetPosts().WithId(2);

ModifyPost(post, "Attributes in C# - UPDATED!");
AddComment(post, "Woah - you updated the post");
AddComment(post, "No way - that's crazy - updating the post");

DisplayPost(post);

Опять ленивая загрузка привела к тому, что мы загружали комментарии, которых не было, когда мы загружали исходное сообщение, но здесь мы имеем ошибку параллелизма — у нас есть 2 набора данных, которые неверны при объединении. Оригинальный пост был изменен, и в результате комментарии не имеют смысла. (Конечно, в этом случае природа комментариев диктует, что они не имеют смысла, но вы поняли)

Конечно, это вряд ли произойдет в случае системы публикаций / комментариев, и даже если это произойдет, результаты не так драматичны. Однако представьте себе больничную систему, в которой список лекарств пациента загружен лениво, или систему учета, в которой расходы на счет лениво загружаются — противоречивые результаты могут иметь серьезные последствия.

так что нам делать?

Какой вывод? Должны ли мы прекратить использовать отложенную загрузку в критических областях? Не обязательно. В приведенных выше примерах пост, который мы отображали, был проблематичным только тогда, когда пост был изменен до того, как мы загрузили комментарии. Изменяя отложенный список, мы можем поддерживать отложенную загрузку, а также избегать проблем параллелизма.

Моя первоначальная идея состояла в том, чтобы присоединиться к таблице публикаций и добавить условие where для отметки времени публикации. Хотя это помешало бы нам загружать любые комментарии, которые не относятся к исходной версии сообщения, если сообщение будет изменено (как во втором примере), в результате просто не будет возвращено никаких комментариев — предложение where будет просто исключить все комментарии. У нас не было бы никакой возможности узнать, имеем ли мы дело с постом без комментариев или проблемой параллелизма.

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

public IList<T2> Inner
{
    get
    {
        if (inner == null)
        {
            using (var transaction = new TransactionScope())
            {
                if (Equals(parentObject.SingleOrDefault(), default(T1)))
                {
                    throw new ChangeConflictException(string.Format("Lazy loading of a list of objects of type {0} failed - the parent object of type {1} was modified", typeof(T2).Name, typeof(T1).Name));
                }
                inner = query.ToList();

                transaction.Complete();
            }
        }
        return inner;
    }
}
private IList<Domain.Comment> GetCommentsForPost(int postId, Binary timestamp)
{
    var parentObject = from post in dataContext.Posts
                       where post.Id == postId && post.Timestamp == timestamp
                       select new Domain.Post();

    var comments = from comment in dataContext.Comments
                   where comment.PostId == postId
                   select new Domain.Comment
                   {
                       Id = comment.Id,
                       Message = comment.Message,
                       CreatedOn = comment.CreatedOn,
                       Timestamp = comment.Timestamp
                   };

    return new LazyList<Domain.Post, Domain.Comment>(parentObject, comments);
}

Я не слишком люблю использовать объект TransactionScope внутри класса Lazy List, но я не вижу другого способа обеспечения параллелизма и все еще использования отложенной загрузки.

Если я запускаю второй пример снова, генерируется исключение.

Последние мысли

Вы можете утверждать, что этот тип параллелизма не нужен — и вы, вероятно, были бы правы. В большинстве приложений эти типы ошибок были бы невероятно редкими, и когда они возникают, пользователю просто нужно нажать обновить. Я только пытаюсь показать способ устранения этого типа ошибок для систем, где этот тип параллелизма необходим.

Author: admin

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *