使用一个DataContext,还是多个?

问题的提出

在前不久的一篇随笔中,我们讨论了是否需要手动执行DataContext的Dispose方法,最终的结论是不需要(即没有必要)。那么我们很自然会想到,既然不需要手动Dispose,那么是不是可以只使用一个DataContext而没有必要每次都new一个新的DataContext呢?比如使用传说中的单例模式。

实际上这样一来就乱套了。

首当其冲的是并发冲突问题。我们知道LINQ to SQL是对ADO.NET的封装,对于查询操作,使用的是DataReader。那么在多线程情况下如果两段代码先后使用同一个DataContext(单例)执行查询操作,必然会抛出DataReader未关闭的异常。

此外,由于DataContext会保存状态,还可能发生很多奇怪的问题

既然使用全局唯一的DataContext是不可取的,那么我们可不可以在每一个数据访问类中使用一个DataContext呢?

public class SingleDataContextProductRepository : IProductRepository
{
    private NorthwindDataContext db;

    public SingleDataContextProductRepository()
    {
        db = new NorthwindDataContext();
    }

    public void InsertProduct(Product p)
    {
        db.Products.InsertOnSubmit(p);
        db.SubmitChanges();
    }

    public void UpdateProduct(Product newProduct)
    {
        db.SubmitChanges();
    }

    public Product GetProduct(int id)
    {
        return db.Products.SingleOrDefault(p => p.ProductID == id);
    }
}

DataContext是轻量级的吗?

浪子同学前不久指出我滥用DataContext,他认为“datacontext是个很大的对象,应该避免不停地实例化”、“一个DataContext至少占有一个dbconnection,所以最好不要到处实例化你的DataContext,性能会有大影响”。

首先,我并不认为“由于DataContext维护一个DbConnection而不能到处实例化”是一个站得住脚的论据。因为DataContext在执行完查询和提交更改的操作之后,都会关闭Connection,一次实例化所带来的开销,与Connection无关。

那么DataContext究竟是不是一个“很大的对象”呢?空口无凭,还是请出源代码来说明问题。

在DataContext的构造函数中,核心的初始化代码都委托给了这样一个私有方法:

private void Init(object connection, MappingSource mapping)
{
    MetaModel model = mapping.GetModel(base.GetType());
    this.services = new CommonDataServices(this, model);
    this.conflicts = new ChangeConflictCollection();
    if (model.ProviderType == null)
    {
        throw System.Data.Linq.Error.ProviderTypeNull();
    }
    Type providerType = model.ProviderType;
    if (!typeof(IProvider).IsAssignableFrom(providerType))
    {
        throw System.Data.Linq.Error.
ProviderDoesNotImplementRequiredInterface(providerType,
typeof(IProvider)); } this.provider = (IProvider) Activator.CreateInstance(providerType); this.provider.Initialize(this.services, connection); this.tables = new Dictionary<MetaTable, ITable>(); this.InitTables(this); }

其中,MappingSource用来设置DataContext的映射方式:通过Attribute(AttrubiteMappingSource)还是通过XML配置文件(XmlMappingSource)。LINQ to SQL分别为这两种映射方式提供了MetaModel、MetaTable、MetaType、MetaParameter、MetaAssociation、MetaFunction等等。

DataContext根据MappingSource获取相应的MetaModel,在此基础上创建Provider和Tables,并进行初始化。在provider.Initialize中,SqlProvider对象对connection进行初始化,但并不Open,在执行查询和更新时才会Open。

可见,DataContext并没有占用多么庞大的资源,无非就是一个DbConnection和一些映射对象而已。

有人说它“大”,我想可能是看到它拥有所有表的映射吧。但那只是映射而已,没有任何数据。

MSDN中的描述是这样的:

A DataContext is lightweight and is not expensive to create. A typical LINQ to SQL application creates DataContext instances at method scope or as a member of short-lived classes that represent a logical set of related database operations.

可见DataContext是轻量级的,创建它不需要很大的开销。

直观上说,由于执行的初始化操作和占用的内存都要少,使用一个DataContext,肯定要比使用多个要“快”。但是在《LINQ in Action》中,却有这样一段话:

If we need to be able to update the data, be aware of the context’s scope and
manage it appropriately. In Windows applications, it may be acceptable to retain a context as changes are made, but realize the performance and memory overhead that come with retaining all objects and the changed values. The intended usage pattern for LINQ to SQL is as a unit of work, which uses the following pattern: Query – Report – Edit – Submit – Dispose. As soon as we no longer need to maintain changes on an object, we should clean up the context and create a new one.

也就是说变化跟踪服务会带来性能和内存的损耗,使用DataContext的推荐模式是将其作为工作单元(unit of work,即查询--显示--修改--提交--销毁。需要的时候就创建,不需要的时候就销毁。

我已经说服自己随时创建DataContext,倒是变化跟踪服务究竟会有多大的影响呢?还是来进行一下性能测试吧。

性能比较 I

之前的测试一样,我们仍然使用Northwind数据库,并建立IProductRepository接口,然后创建SingleDataContextProductRepository和MultiDataContextProductRepository,并且捎带着测试一下释放DataContext的性能,创建一个DisposedDataContextProductRepository。在MultiDataContextProductRepository中,我们使用前面测试过的最快的委托方法

所有的代码参见文后的下载链接。测试结果见下面的图。

循环1000次:

 image

循环10000次:

image

可见单一DataContext的性能还是比多个DataContext高出不少,但恐怕远远不到“滥用”带来的毁灭性的影响。而是否Dispose反而对性能的影响并不十分明显。

性能比较 II

以上测试是在没有关联对象的情况下进行的,那么存在关联关系的情况下结果如何呢?而且以上查询的是单一的结果,如果取回一个集合,情况又会发生什么变化呢?我们假设要得到一个IEnumerable<Product>,并遍历这个集合,访问每一个Product的Category属性。因此在IProductRepository接口中添加一个GetProducts方法。

IEnumerable<Product> GetProducts(Expression<Func<Product, bool>> predicate);

由于DataContext在Dispose之后无法进行延迟加载,我们只能在GetProducts中通过DataLoadOptions对象将关联关系一次性加载进来,并在返回结果时调用ToList方法。

public IEnumerable<Product> GetProducts(Expression<Func<Product, bool>> predicate)
{
    using (NorthwindDataContext db = new NorthwindDataContext())
    {
        DataLoadOptions loads = new DataLoadOptions();
        loads.LoadWith<Product>(p => p.Order_Details);
        loads.LoadWith<Product>(p => p.Category);
        loads.LoadWith<Product>(p => p.Supplier);
        db.LoadOptions = loads;
        return db.Products.Where(predicate).ToList();
    }
}

结果会如何呢?循环1000次:

image

Dispose方法由于加载了大量关联对象,导致性能大幅下降。有没有避免使用DataLoadOption加载关联对象的方法呢?我暂时没有想到,有知道的朋友不妨提示一下。

如何取舍

单从这个测试来看,一个DataContext明显要优于多个DataContext。而且我没有看出变化跟踪服务对性能的影响。也许这个例子并不合适,也请大家指出。在实际开发中,情况是否会有变化,也有待验证。

我想要说明的还是立即加载的问题。由于一个DataContext只能设定一次DataLoadOptions,那么如果需要立即加载关联对象时,就只能在SingleDataContext的构造函数中设置LoadOptions,这样整个性能将不堪设想。

MSDN中对于DataContext的说明是:用的时候创建,不用的时候销毁。在多层架构中,“不用的时候销毁”这条已经被证明是低效的了,但本文的测试还无法完全否定“用的时候创建”这条建议。

如果你有什么好的想法,请一定要告诉我。

代码下载

posted @ 2010-01-29 16:52  麒麟.NET  阅读(9349)  评论(17编辑  收藏  举报