并行性和实体框架

6

在我们的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,但这会很快使代码变得混乱。我可以为每个查询创建一个包装函数,该函数使用自己的上下文,但这感觉很凌乱,对于解决问题来说也不是长期的好方法。
如何解决这个问题?我希望客户端/消费者在执行多个查询时无需担心处理每个仓库/上下文。
我现在唯一的想法是遵循类似工厂模式的方法,除了我的工厂将跟踪它创建的所有对象。然后,一旦知道我的查询已完成,就可以处理工厂,并且工厂可以在内部处理每个仓库/上下文。
我很惊讶在并行和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);
    }
}

正如您所见,每个存储库都有自己的上下文。这意味着每个存储库都需要被处理。就我上面给出的示例而言,这意味着我需要调用 Dispose() 函数 4 次。

对于这个问题,我的想法是采用工厂模式,具体如下:

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
@usr 我们在生产环境中有一些由其他人编写的代码,这些代码没有正确处理所有的上下文。:) 它能正常工作,但我不确定会有什么后果。由于上下文实现了IDisposable接口,我希望能够开发一种方法来消除可能发生的未知情况。 - Justin Helgerson
1
“解决这个问题的最佳方式是什么?” - 我认为您需要更具体地说明您对已经确定的可能方法的反对意见。比“混乱”和“杂乱”更好的东西。事实上,封装是一种常见且有效的技术,用于隐藏“混乱”和“杂乱”,而某种包装器则是一种封装形式。如果没有更多细节,您将得到模糊、主观的答案。 - Peter Duniho
@PeterDuniho - 说得好。我对为每个存储库方法编写包装器方法的反对意见是,它感觉非常重复。我很想听听人们对处理此问题的工厂式方法的看法。我认为这个问题清楚地显示了问题所在以及我试图解决的问题。软件工程涉及解决问题的观点,因此如果有人对如何最好地解决问题有意见,我很乐意听取。 - Justin Helgerson
在你的示例中,上下文是在何时何地创建并不清楚。你说每个请求只有一个上下文,但每个存储库又会创建自己的上下文? - ken2k
显示剩余3条评论
1个回答

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位。
您的查询方法仍然是简单的一行代码,客户端可以非常容易地根据需要配置处置行为,甚至在自动处置情况下重复使用存储库实例。
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);

这是一个好主意,起初我也是这样想解决这个问题的,但是有时候我不能将上下文处理掉,因为需要更新记录。有很多情况下我们需要从数据库查询现有的记录,使用用户提供的新数据更新对象,并提交这些更改到数据库。如果上下文被处理掉,那么更新将会失败。 - Justin Helgerson
好的,听起来你需要可配置的行为,基于此我重新编写了我的答案。我仍然认为这里没有必要使用工厂类。 - Todd Menier
这看起来不错!我不确定为什么我没有想到把那一点移到构造函数中。我给你一个+1,希望今天下午或下周试一试这个。 - Justin Helgerson
后来我想到一个稍微不同的变化,就是使用一个独立的类(比如 ConcurrentRepository)代替 autodispose。 实现方式类似于上面的方法,只需将 WithContext 作为受保护的虚拟方法,仅使用共享上下文实现它,并使 ConcurrentRepository 继承自 Repository,并使用 using 实现覆盖 WithContext。相同的DRY量,唯一的优点是如果您认为实例化不同的类比传递构造函数参数更清晰,则可以选择此方法。 - Todd Menier
谢谢你的帮助,托德!我终于能够回来处理这个问题了,现在它运行得非常好。 - Justin Helgerson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接