2015-04-15 2 views
5

Очень часто в наших веб-приложениях нужны данные из разных таблиц в нашей базе данных. Сегодня вы можете найти 5 или 6 запросов к базе данных, которые выполняются последовательно для одного запроса. Ни один из этих запросов не зависит от данных от другого, поэтому они являются идеальными кандидатами для параллельного выполнения. Проблема заключается в хорошо известном DbConcurrencyException, который генерируется, когда несколько запросов выполняются в одном контексте.Parallelism and Entity Framework

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

Ниже приведен пример, в котором используется параллелизм, но все еще есть проблема!

var fileTask = new Repository().GetFile(id); 
var filesTask = new Repository().GetAllFiles(); 
var productsTask = AllProducts(); 
var versionsTask = new Repository().GetVersions(); 
var termsTask = new Repository().GetTerms(); 

await Task.WhenAll(fileTask, filesTask, productsTask, versionsTask, termsTask); 

Каждый репозиторий внутренне создает свой собственный контекст, но, как и сейчас, они не располагаются. Это проблема. Я знаю, что могу позвонить Dispose в каждый репозиторий, который я создаю, но это начинает быстро загромождать код. Я мог бы создать функцию-оболочку для каждого запроса, который использует свой собственный контекст, но это кажется беспорядочным и не является большим долгосрочным решением проблемы.

Что было бы лучшим способом решить эту проблему? Я бы хотел, чтобы клиент/потребитель не беспокоился об утилизации каждого репозитория/контекста в случае одновременного выполнения нескольких запросов.

Единственная идея, которую я имею сейчас, - это следовать подходу, подобному заводской схеме, за исключением того, что мой завод будет отслеживать все созданные им объекты. Тогда я мог бы продать завод, как только я узнаю, что мои запросы завершены, и фабрика может внутренне распоряжаться каждым репозиторием/контекстом.

Я удивлен увидеть такую ​​маленькую дискуссию вокруг параллельности и Entity Framework, так что мы надеемся, еще некоторые идеи из сообщества придут.

Редактировать

Вот простой пример того, что наш репозиторий выглядит так:

public class Repository : IDisposable { 
    public Repository() { 
     this.context = new Context(); 
     this.context.Configuration.LazyLoadingEnabled = false; 
    } 

    public async Task<File> GetFile(int id) { 
     return await this.context.Files.FirstOrDefaultAsync(f => f.Id == id); 
    } 

    private bool disposed = false; 

    protected virtual void Dispose(bool disposing) { 
     if (!this.disposed) { 
      if (disposing) { 
       context.Dispose(); 
      } 
     } 
     this.disposed = true; 
    } 

    public void Dispose() { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
} 

Как вы можете видеть, каждый репозиторий получает свой собственный контекст. Это означает, что каждый репозиторий необходимо утилизировать. В приведенном выше примере, это означает, что мне понадобится 4 обращения к Dispose().

Мои мысли для фабричного подхода к проблеме было что-то вроде следующего:

public class RepositoryFactory : IDisposable { 
    private List<IRepository> repositories; 

    public RepositoryFactory() { 
     this.repositories = new List<IRepository>(); 
    } 

    public IRepository CreateRepository() { 
     var repo = new Repository(); 
     this.repositories.Add(repo); 
     return repo;    
    } 

    #region Dispose 
    private bool disposed = false; 

    protected virtual void Dispose(bool disposing) { 
     if (!this.disposed) { 
      if (disposing) { 
       foreach (var repo in repositories) { 
        repo.Dispose(); 
       } 
      } 
     } 
     this.disposed = true; 
    } 

    public void Dispose() { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
    #endregion 
} 

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

+0

Я считаю, контекст EF не нужно удалять, если вы не управляете соединением вручную. Он должен открываться и закрываться для каждого запроса. Однако бесполезные контексты нападают на меня как на грязный подход. – usr

+0

@usr У нас есть код, написанный кем-то еще в производстве, который не избавляет от всех контекстов. :) Это работает, но я не уверен, какие последствия будут или будут. Поскольку контекст реализует 'IDisposable', я хотел бы разработать подход, который устраняет тайну того, что может случиться. –

+1

_ «Какой был бы лучший способ решить эту проблему?» _ - Я думаю, вам нужно будет более подробно рассказать о своих возражениях относительно возможных подходов, которые вы уже определили. Что-то лучше, чем «грязный» и «беспорядок». Факт заключается в том, что инкапсуляция является распространенной и достоверной техникой для скрытия «беспорядочного» и «беспорядочного», а оболочка какого-то рода - это форма инкапсуляции. Без более подробной информации все, что вы собираетесь получить, - это расплывчатые, упрямые ответы. –

ответ

1

Вы можете разрешить клиентам конфигурировать поведение удаления Repository, передав конструктору необязательный (по умолчанию по умолчанию) autodispose бит. Реализация будет выглядеть примерно так:

public class Repository : IDisposable 
{ 
    private readonly bool _autodispose = false; 
    private readonly Lazy<Context> _context = new Lazy<Context>(CreateContext); 

    public Repository(bool autodispose = false) { 
     _autodispose = autodispose; 
    } 

    public Task<File> GetFile(int id) { 
     // public query methods are still one-liners 
     return WithContext(c => c.Files.FirstOrDefaultAsync(f => f.Id == id)); 
    } 

    private async Task<T> WithContext<T>(Func<Context, Task<T>> func) { 
     if (_autodispose) { 
      using (var c = CreateContext()) { 
       return await func(c); 
      } 
     } 
     else { 
      return await func(_context.Value); 
     } 
    } 

    private static Context CreateContext() { 
     var c = new Context(); 
     c.Configuration.LazyLoadingEnabled = false; 
     return c; 
    } 

    public void Dispose() { 
     if (_context.IsValueCreated) 
      _context.Value.Dispose(); 
    } 
} 

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

Ваших методов запросов по-прежнему простые однострочечники, и клиент может легко настроить поведение удаления по мере необходимости, и даже повторно использовать Repository экземпляр в авто-удалении ситуаций:

var repo = new Repository(autodispose: true); 
var fileTask = repo.GetFile(id); 
var filesTask = repo.GetAllFiles(); 
var productsTask = AllProducts(); 
var versionsTask = repo.GetVersions(); 
var termsTask = repo.GetTerms(); 

await Task.WhenAll(fileTask, filesTask, productsTask, versionsTask, termsTask); 
+0

Это хорошая идея, и изначально я решил, что могу решить эту проблему, но бывают случаи, когда я не могу использовать контекст, потому что запись должна быть обновлена. Существует много ситуаций, когда мы запрашиваем существующую запись из базы данных, обновляем объект новыми данными от пользователя и фиксируем эти изменения в базе данных. Если контекст будет удален, обновление завершится ошибкой. –

+0

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

+0

Это выглядит хорошо! Я не уверен, почему я не думал переместить этот бит в конструктор. Я дал вам +1, и я, надеюсь, дам это попробовать позже сегодня днем ​​или на следующей неделе. –

Смежные вопросы