【问题标题】:Parallelism and the Entity Framework并行性和实体框架
【发布时间】:2015-04-15 15:13:01
【问题描述】:

在我们的 Web 应用程序中,需要来自数据库中各种表的数据是很常见的。今天,您可能会发现单个请求会连续执行 5 或 6 个数据库查询。这些查询都不依赖于其他查询的数据,因此它们是并行执行的完美候选者。问题是众所周知的DbConcurrencyException,当针对同一上下文执行多个查询时会抛出该问题。

我们通常为每个请求使用一个上下文,然后有一个存储库类,以便我们可以在各种项目中重用查询。然后,当控制器被释放时,我们会在请求结束时释放上下文。

下面是一个使用并行的例子,但还是有问题!

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,但这开始很快使代码混乱。我可以为每个使用自己的上下文的查询创建一个包装函数,但这感觉很混乱,并且不是解决问题的一个很好的长期解决方案。

解决此问题的最佳方法是什么?我希望客户端/消费者不必担心在并行执行多个查询的情况下处理每个存储库/上下文。

我现在唯一的想法是遵循类似于工厂模式的方法,除了我的工厂会跟踪它创建的所有对象。一旦我知道我的查询已完成,我就可以处置工厂,并且工厂可以在内部处置每个存储库/上下文。

我很惊讶看到围绕并行性和实体框架的讨论如此之少,因此希望社区能提供更多想法。

编辑

以下是我们的存储库的简单示例:

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
}

这个工厂将负责创建我的存储库的实例,但它也会跟踪它创建的所有实例。一旦这个工厂类被处理掉,它将在内部负责处理它创建的每个存储库。

【问题讨论】:

  • 我相信如果您不手动管理连接,则不需要释放 EF 上下文。它应该为每个请求打开和关闭。不过,不处理上下文让我觉得这是一种肮脏的方法。
  • @usr 我们有其他人在生产环境中编写的代码,但并未处理所有上下文。 :) 它有效,但我不确定后果是什么或将会是什么。由于上下文实现了IDisposable,我想开发一种方法来消除可能发生的事情的奥秘。
  • “解决这个问题的最佳方法是什么?” -- 我认为您需要更具体地说明您对已经确定的可能方法的反对意见.比“凌乱”和“杂乱”更好的东西。事实上,封装是隐藏“混乱”和“混乱”的一种常见且有效的技术,某种包装器是一种封装形式。如果没有更多细节,您将得到的只是模糊的、固执己见的答案。
  • @PeterDuniho - 公平点。我反对为每个存储库方法编写包装器方法是因为它感觉非常重复。我很想知道人们对处理这个问题的工厂式方法有何看法。我认为这个问题清楚地表明了问题所在以及我要解决的问题。软件工程对如何解决问题有意见,所以如果有人对如何最好地解决问题有意见,我很乐意听到。
  • 在您的示例中不清楚创建上下文的位置和时间。你说每个请求都有一个上下文,但每个存储库都会创建自己的上下文?

标签: c# entity-framework parallel-processing async-await


【解决方案1】:

您可以允许客户端通过将某种可选的(默认为 false)autodispose 位传递给构造函数来配置 Repository 的处置行为。实现看起来像这样:

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);

【讨论】:

  • 这是个好主意,最初我认为我可以解决这个问题,但有时我无法处理上下文,因为需要更新记录。在很多情况下,我们从数据库中查询现有记录,使用来自用户的新数据更新对象并将这些更改提交到数据库。如果上下文被释放,那么更新将失败。
  • 好的,听起来您需要可配置的行为,并基于此我重新编写了我的答案。我仍然认为这里不需要工厂类。
  • 这看起来不错!我不确定为什么我不认为将那一点移到构造函数中。我给了你 +1,希望今天下午晚些时候或下周试一试。
  • 我后来想到的一个细微变化是有一个单独的类(类似于ConcurrentRepository)而不是autodispose 位。实现将与上面类似,您只需将WithContext 设为受保护虚拟,仅使用共享上下文实现它,并让ConcurrentRepository 继承自Repository 并使用using 实现覆盖WithContext。相同数量的 DRY,唯一的好处是如果您认为实例化不同的类对客户端来说比传递构造函数位更干净。
  • 感谢您的帮助托德!我终于能够回到这一点,而且效果很好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-03
  • 2013-08-13
  • 2012-12-27
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
相关资源
最近更新 更多