• 如何在 ASP.NET项目里面正确使用Linq to Sql(转)


    原文地址:http://www.cnblogs.com/sumtec/archive/2009/05/31/1285331.html

    老久不上来写技术类的东西了,偶尔回归一下吧。(其实,这篇文章8个月前写了个大半,后来一直没有时间去完善,再后来就因为各种原因给放下来了。)

    非常抱歉,由于需要发表其他文章的缘故,我只能忍着不修正文中 一小部分错误,以及增加一些有助免于误解的内容。这里特别说明一下,本文不是要讨论缓存机制的好坏,更不是要讨论如何缓存对象。而是说DAL/BLL上面 对DataContext的处理。另外一个需要注意的 地方,是修改了一个错误,原来大部分都写成IQueryable了,实际上应该是除了最后一个之外,都是IEnumerable。原因是什么需要大家想一 下。此外,也需要大家注意的事,我所提出来的缓存,并不是直接利用Linq2Sql的代码来缓存,而是指是否便于缓存。这部分的内容,会在文章后面补充说 明。

    Linq to Sql 用的人也应该有些吧,我在cnblogs上面看老赵写的那几篇文章(请看08年9月左右的文章),感觉也很有深度,有不少启发。因此我也打算写一点我自己 的实践经验,希望也能同样给大家一些有用的启发吧。

    我首先想要问一下大家,Linq to Sql有哪些很特别的地方?这个问题的答案肯定五花八门,我说一下我看到的一些问题吧。

    首先,Linq to Sql的基础之一是DataContext,而另外一个基础,则是通过映射产生的实体类,以及这些实体类的Table<>对象。这个不是废话 嘛!我想很多人都应该知道这个最基本的知识,不过却不见得有多少人真正注意到,或者认真思考一下这里面的“机关”。不知道“机关”在哪里,那么就不可能写 出合适的代码。比如说,在某个页面里面(N层结构没有给弄好的情况下),或者在某个业务逻辑里面(有N层结构),你的Linq to Sql的代码是否是长这样的?

    Code (MyDataContext db = new MyDataContext)
    {
      var q = from product in db.ProductInfos
              
    where product.Price > 100
              select product;
      DoSomethingWithProducts(q.ToList());
    }

    “对啊,就是长这样的,有什么问题吗?”当然有问题啦,否则我也不写这个随笔了。不知道大家有没有想过这么一个问题,什么叫做 Context?Context就是上下文,上下文的意思就是,依赖于这个上下文的对象,必须存活在这个上下文里面。脱离了这个上下文,那些对象就会出现 错误。事实上也确实如此:在上面的例子里面,从ProductInfos中得到的q.ToList(),里面的每一个元素都依赖于 MyDataContext。换句话说MyDataContext如果被注销了,q.ToList()生成的对象也就会“部分功能失效”。

    “失效就失效好了,反正该做的工作已经做完了,q.ToList()也已经利用完了。”不错,在上面的例子里面,不会发生什么错误。不过这么写的 话,会比较难使用的。为什么这么说?我举一个具体的例子:这个网站需要用户登录,而所有的业务逻辑几乎都依赖于当前用户。如果说,我们使用上面的 using模式,那么我估计你的代码不外乎是如下两种情况:

    1、每一次需要当前用户的地方,你都需要从数据库读取;或者

    2、你把当前用户保存为全局变量了,但是你发现currentUser.CompanyInfo因为上下文已经抛弃了,因此是无法使用的,业务层不 得不每一次都重新从数据库读取该用户所属公司的数据。

    这两种形式如下所示:

    Code 通过实体对象来存储
    // 注意!这个函数是不在BLL层的,而是更上层的某些处理,为了演示起来简单,显得是BLL层的东西。
    public double GetCurrentBalanceByObject()
    {
       
    int userId;
       
    int.TryParse(HttpContext.Current.User.Identity, out userId);
       UserInfo user = GetUser(userId);
       CompanyInfo company = GetCompanyByUser(user);
       IEnumerable<TransactionInfo> transactions = GetTransactionsByCompany(company);
       
    return transactions.Sum(item => item.Amount);
    }


    // 后面这几个方法才是BLL层 的东西,后面的例子也相同。
    public UserInfo GetUser(int userId)
    {
       
    using(MyDataContext context = new MyDataContext)
       {
          
    return context.UserInfos.Where(item => item.UserId == userId).FirstOrDefault();
       }
    }

    public CompanyInfo GetCompanyByUser(UserInfo user)
    {
       
    using(MyDataContext context = new MyDataContext)
       {
          
    return context.CompanyInfos.Where(item => item.UserId == user.Id).FirstOrDefault();
       }
    }

    public IEnumerable<Transaction> GetTransactionsByCompany(CompanyInfo company)
    {
       
    using(MyDataContext context = new MyDataContext)
       {
          
    return context.TransactionInfos.Where(item => item.CompnayId == company.Id).ToList;
       }
    }

    // 实际上很容易就 退化为通过键值来存储,因为在这种设计方式下面,
    // 实际上根本没有什么必要去传输整个对象。
    // 我们可以想象,这个时候很多的操作其实是依赖UserId和CompanyId的,
    // 而我见过的“有趣”设计,是在Page_Load事件中,不管是否需要用到,
    // 都会将 HttpContext.Current.User.Identity以及
    // GetCompanyByUserId(userId).CompanyId 保存为当前页面的全局变量。
    // 其实这样是违背了Linq的设计初衷的。
    // 当然,我不是说不缓存最经常用 的东西,该缓存还是要缓存的,只是,
    // 下面的设计只会把问题变复杂。因为这里只能缓存一个id值,内部还是要重复获取User或者 Company对象的。
    // 下面就是一个只传Id的做法:
    public double GetCurrentBalanceByObject()
    {
       
    int userId;
       
    int.TryParse(HttpContext.Current.User.Identity, out userId);
       CompanyInfo company = GetCompanyByUserId(userId);
       IEnumerable<TransactionInfo> transactions = GetTransactionsByCompanyId(company.CompanyId);
       
    return transactions.Sum(item => item.Amount);
    }

    public CompanyInfo GetCompanyByUser(int userId)
    {
       
    using(MyDataContext context = new MyDataContext)
       {
          
    return context.CompanyInfos.Where(item => item.UserId == userId).FirstOrDefault();
       }
    }

    public IEnumerable<Transaction> GetTransactionsByCompanyId(int companyId)
    {
       
    using(MyDataContext context = new MyDataContext)
       {
          
    return context.TransactionInfos.Where(item => item.CompnayId == companyId).ToList();
       }
    }

    如果你是第一种情况,那么很明显,你会有大量重复的SQL调用。

    如果是第二种情况,其实也不见得好到哪里去。因为:

    1、currentUser可能不需要经常取,但相关的其它内容,由于上下文各自独立,你还是经常在重复的获取的;

    2、有一个地方你无法绕过去——如果你要修改当前用户的属性,而这个全局的当前用户不是当前Context产生的,你还非得从当前Context取 出来,然后再修改;或者如果你企图通过currentUser.CompanyInfo来访问的话,也会报错。

    3、因为currentUser的上下文已经被抛弃了,因此程序会很容易设计成传入的不是一个UserInfo,而是一个int类型的Id值,否则 底层很容易一不小心就用到这个实际上功能不全的对象,然后就抛出异常了。但这样做的后果是,获取同一个类型的实体对象,可能会有各种不同的重载形式,例 如:

    IEnumerable<TransactionInfo> GetTransactionsByUserId(int userId);
    IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId);
    IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId, EAccountName account);
    IEnumerable<TransactionInfo> GetTransactionsByCompanyId(int companyId, EAccountName account, ETransactionType transactionType);

    因为这种设计实施之后,有时很可能就会出现只有userId的情况,那么这个时候即使UserInfo对象中其实也存在CompanyId的值,也 还是要重新获取一遍UserInfo对象。(注意,后面提到的方案, 并非不需要缓存了,而是因为在同一个Context下,可以有效地利用实体对象,因此你可以将BLL层设计依赖实体对象,而不是id值,因此缓存整个实体 对象会更加容易。)为了简化这一过程,就可能会产生不同的获取形式。

    这样设计完整个系统之后一跑,看着好像没什么,但真正上线却发现有点慢。当我们打开Sql server的Profiler一看,会发现很简单的一个页面的访问,其数据库访问会搞到几十次甚至上百次,其中有很多Sql语句是完全重复的。

    这个问题怎么解决呢?有人会说,加个缓存机制吧。也许吧,但这种增加复杂度的设计,我觉得还是不得已而为之的一种做法。(或者说,缓存很好,那是另 外一种解决方案,但不解决这个问题。)我认为更好的解决办法是,将上下文在当前页面中缓存起来。所谓的上下文,就是一种运行环境,而一次页面访问,其环境 应该是相同的。首先,我们对MyDataContext做一个扩展:

    Codepartial class MyDataContext
        {
            
    private const string c_KeyCurrentHttpContext = "chctx";

            
    static public MyDataContext CurrentHttpContext
            {
                
    get
                {
                    MyDataContext context = CurrentHttpContextWeak;
                    
    if (context == null)
                    {
                        context = new MyDataContext();
                        CurrentHttpContextWeak = context;
                    }
                    
    return context;
                }
            }

            
    static private MyDataContext CurrentHttpContextWeak
            {
                
    get
                {
                    
    return HttpContext.Current.Items[c_KeyCurrentHttpContext] as MyDataContext;
                }
                
    set
                {
                    HttpContext.Current.Items[c_KeyCurrentHttpContext] = value;
                }
            }

            
    static internal void TryDisposeCurrentHttpContext()
            {
                MyDataContext context = CurrentHttpContextWeak;
                
    if (context != null)
                {
                    context.Dispose();
                    CurrentHttpContextWeak = null;
                }
            }
        }

    然后我们再制作一个HttpModule(并且在web.config里面配置好):

    Code/// <summary>
        
    /// 实现自 动抛弃当前数据库上下文的模块
        
    /// </summary>
        public class MyDataContextAutoDisposeModule : IHttpModule
        {
            
    #region IHttpModule Members

            
    private HttpApplication _context;
            
    public void Init(HttpApplication context)
            {
                _context = context;
                context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
            }

            
    void context_PostRequestHandlerExecute(object sender, EventArgs e)
            {
                MyDataContext.TryDisposeCurrentHttpContext();
            }

            
    #endregion
        }

    接下来,我们只要在逻辑层这么直接写即可:

    Codepublic static IQueryable<TransactionInfo> GetCompanyAccountDetails(UserInfo operatorUser, EAccountName account)
            {
                
    // 权限验证
                if (!operatorUser.Permissions.Contains(EUserPermissions.ViewAccountDetails))
                    CLog.CurrentHttpContext.ThrowFailedException(new CPermissionException(EUserPermissions.ViewAccountDetails));

                var q = MyDataContext.CurrentHttpContext.TransactionInfos.Where(t => t.CompanyId == operatorUser.CompanyId && t.AccountName == account);
                
    return q;
            }

    这么改造完之后,你会发现几乎可以在所有地方直接返回IEnumerable(除了有的时候Linq to Sql本身有Bug),整个逻辑层内的设计变得简单化:一开始检查各种参数(是否具备完整或者部分权限),然后根据检查结果做完全信赖的操作。由于返回的 是实体对象,或者IEnumerable,几乎所有重复性的Sql调用也随之自然消失了。如果有所怀疑的话,您可以用Sql Profiler自行做修改前后的对比,看看效果是否“惊人”?

    也许有人会质疑,这样好吗?岂不是通过user.Company.Transactions就可以得到所有的Transaction了?没错,如果 所有东西都是公开的话,就会有这个问题。如果要彻底解决这样的问题,需要将这些部分变成对逻辑层可见,而对其它层不可见的修饰方式——比如两层在一个 dll里面,这些属性是internal的,或者放在两个dll里面并且打上InternalsVisibleTo标记。通过这种方式,就可以避免上层直 接查找DAL中一些在BLL中需要经过权限检查才可以得到的内容。当然,如果项目比较小的情况下,你也可以选择不要这么麻烦,直接控制代码质量即可(要求 有些东西必须通过BLL来获得)。

    _________________________________________

    后记:

    因为中间跳过了一些产生问题的步骤,引起了不少的误解,这里特别解释清楚:

    1、原方案因为不在同一个Context之下,所以返回的实体对象是不分功能失效的。考虑:

    _user = BLL.GetUser(HttpContext.Current.User.Identity);

    这样的缓存,由于DataContext在GetUser中已经抛弃了,因此,_user.Company这样的访问就会报错。最终你更可能选择, 要么全面放弃实体对象之间的关联属性,要么就只是缓存userId。无论哪一种方案,都意味着,当你缓存userId的时候,是不会自动缓存对应的 CompanyInfo的。(这么说明报了吧?这才是我说的,可以自动缓存的意思。)

    关于这一部分的误解,回复中有一个例子:

    public static T_User TestMethod2()
    {
        return dbUserDataContext.CurrentHttpContext.T_User.FirstOrDefault();
    }

    // 调用代码
    for (int i = 0; i < 5; i++)
    {
        BLL.dbUser.TestMethod2();
    }

    这样显然是会重复发出Sql调用的,这不是我说的场景,我说的是:

    public static T_User TestMethod2()
    {
        return dbUserDataContext.CurrentHttpContext.T_User.FirstOrDefault();
    }

    // 调用代码

    _user =  BLL.dbUser.TestMethod2();  // 缓存
    for (int i = 0; i < 5; i++)
    {

        Debug.Writeline(_user.Company.CompanyName); // 这个时候Company就不会不停的调数据库
    }

    从这个角度来讲,我并没有讨论如何缓存,而是讨论的如何便于缓存,如果在不需要过高性能要求的情况下,不动用完整的、同时也很复杂的缓存机制。

    2、我们再看另外一个例子:

    http://www.cnblogs.com/JeffreyZhao/archive/2008/02/19/using-translate-method-and-modify-command-text-before-query-in-linq-to-sql.html

    public List<Item> GetItemsForListing(int ownerId)
    {
        ItemDataContext
     dataContext = new ItemDataContext();
        var
     query = from item in dataContext.Items
                    where
     item.UserID == ownerId
                    orderby
     item.CreateTime descending
                    select
     item;
     
        return
     query.ToList();
    }

    大家可以看到,这个设计里面,返回至十一个ToList(),也就意味着已经从数据库中取出所有对象了。但是有的时候,我们实际上希望在这个基础上 再进一步收缩。在这种情况下,我们就不能很好的重用原来的代码了,因为这个函数返回的是一个组内存对象,而不是一个IQueryable的表达式。那么我 们要么在这个List<Item>结合中在内存中进行过滤,要么重新写一个方法来处理。无论如何,这都导致了BLL层的臃肿。如果是如下的写 法呢:

    public IQueryable<Item> GetItemsForListing(User owner)
    {
        ItemDataContext
     dataContext = new ItemDataContext();
        var
     query = from item in dataContext.Items
                    where
     item.UserID == owner.Id
                    select
     item; 
        return
     query;
    }

    注意,我连order by 也去掉了,因为这完全可以在更上一层再决定如何处理。同样的,我也可以在这个返回值得基础上继续做进一步的筛选。而这种筛选无论在BLL还是更高层次来 做,都会有着更高的效率。(说到这里,我也不得不担心,有人会说,这种筛选应该被封闭起来。这部分是另外一种讨论了:BLL到底应该封装到什么程度。这里 不作过多的争论了,我们假定轻量级的BLL更有灵活性。不过如果非要争论,我会说:首先,原来的设计,你也无法避免UI层用这个函数的返回值来做自己的筛 选和处理;其次,我的设计,也完全可以在BLL提供进一步的标准筛选函数。因此这也并非我要讨论的内容。)

    那么,这么做有什么好处呢?最大的好处是,提供同样级别功能的BLL情况下,我的设计更可能只去所需要的数据——严格的说应该限定为,在只访问一次 数据库的情况下。其他情况下也有可能和原来设计一样,需要依赖缓存来提高性能。不过实战情况下,可以发现这么做之后一般都是获取更少的数据。

    无论如何,减少从数据库取出的不必要数据,肯定是一件好事。如果加上缓存机制的话,更少需要缓存的数据,也可能提供更高的效率。

    3、有一点不需要多说了,就是如果要加入缓存机制的话,在同一个Context下面肯定比在不同Context下面好处理得多,最简单的一个原因, 就是不需要考虑Detach和Attach的问题(偏偏Linq2Sql不存在Detach的方法,除非你自己做每一个对象的深拷贝工作,这拜 Linq2Sql的Tracker所赐,具体这也不讨论了)。

    回复中还有一种说法,认为可以尽量将操作都封装到BLL层,例如:

    using (new context)
    {
    receiveRelatedData()
    using (new tansation)
    {
    action1 (context);
    action2 (context);
    action3 (context);
    }
    }

    例子中,并没有很清晰的指出,是整个代码在BLL中呢,还是只有receiveRelatedData和actions是BLL。无论如何,我想这 是另一个问题了,就是BLL应该复杂到什么程度。我个人的理念是:在保证逻辑完整的情况下,尽量提供最小的原子操作,让上层自由互相组合。这样可以避免 BLL越来越臃肿,甚至有一些功能会逐渐被抛弃,进而被遗忘的问题。例如,我会提供获取当前用户、某用户购买一个产品,这样两个原子操作,来达到当前用户 购买产品的目的。否则,可以想象,让如果要让管理员强制另一个用户购买一个产品,将会需要另一整套的逻辑。而按照我的思路,只要设计一个管理员获取另一个 管理员,以及代理权限系统即可,而不需要连后面的购买过程也封装起来。或者说,前一种思路容易导致BLL函数数量呈N*M的方式增长,而我的思路则是尽可 能让BLL按照N+M的方式增长。(这个如果要讨论的话,还是另外开篇把,这里可能说不完。)

    不过即使是这样,我也没有看出为何不可直接将Context缓存起来?比如:

    receiveRelatedData(CurrentContext)
    using (new tansationScope())
    {
      action1 (CurrentContext);
      action2 (CurrentContext);
      action3 (CurrentContext);

    还是一样可以跑得通吧? 

    简而言之,本文并非否认缓存机制的作用,更不是要替代缓存机制。与之正好相反,这里面是要让缓存变得更容易一些,同时在一定范围内,提高不使用复杂 缓存机制时的效能,推迟系统变得更复杂的时间。

    作者:KKcat
        
    个人博客:http://jinzhao.me/
        
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    升级 asp.net core 1.1 到 2.0 preview
    【asp.net core】Publish to a Linux-Ubuntu 14.04 Server Production Environment
    CEF 各个版本适应的平台参考表
    VC2012编译CEF3-转
    【WebKit内核 CEF3 】 第一篇:下载分支代码并本地编译
    INNODB自增主键的一些问题 vs mysql获得自增字段下一个值
    Mysql的批量导入类 MySqlBulkLoader
    一个产生随机数字 字符串验证码 日期的类扩展实现
    asp web api json 序列化后 把私有字段信息也返回了解决办法
    使用Elasticsearch 与 NEST 库 构建 .NET 企业级搜索
  • 原文地址:https://www.cnblogs.com/jinzhao/p/1710340.html
Copyright © 2020-2023  润新知