艾伟:如何在ASP.NET项目里面正确使用Linq to Sql

简介: 老久不上来写技术类的东西了,偶尔回归一下吧。(其实,这篇文章8个月前写了个大半,后来一直没有时间去完善,再后来就因为各种原因给放下来了。)Linq to Sql 用的人也应该有些吧,我在cnblogs上面看老赵写的那几篇文章(请看08年9月左右的文章),感觉也很有深度,有不少启发。

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

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

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

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

Code
using (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
// 通过实体对象来存储
public double GetCurrentBalanceByObject()
{
   
int userId;
   
int.TryParse(HttpContext.Current.User.Identity, out userId);
   UserInfo user 
= GetUser(userId);
   CompanyInfo company 
= GetCompanyByUser(user);
   IQueryable
<TransactionInfo> transactions = GetTransactionsByCompany(company);
   
return transactions.Sum(item => item.Amount);
}

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 IQueryable<Transaction> GetTransactionsByCompany(CompanyInfo company)
{
   
using(MyDataContext context = new MyDataContext)
   {
      
return context.TransactionInfos.Where(item => item.CompnayId == company.Id);
   }
}

// 实际上很容易就退化为通过键值来存储,因为在这种设计方式下面,
// 实际上根本没有什么必要去传输整个对象。
// 我们可以想象,这个时候很多的操作其实是依赖UserId和CompanyId的,
// 而我见过的“有趣”设计,是在Page_Load事件中,不管是否需要用到,
// 都会将HttpContext.Current.User.Identity以及
// GetCompanyByUserId(userId).CompanyId保存为当前页面的全局变量。
// 其实这样是违背了Linq的设计初衷的。
// 下面就是一个只传Id的做法:
public double GetCurrentBalanceByObject()
{
   
int userId;
   
int.TryParse(HttpContext.Current.User.Identity, out userId);
   CompanyInfo company 
= GetCompanyByUserId(userId);
   IQueryable
<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 IQueryable<Transaction> GetTransactionsByCompanyId(int companyId)
{
   
using(MyDataContext context = new MyDataContext)
   {
      
return context.TransactionInfos.Where(item => item.CompnayId == companyId);
   }
}

 

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

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

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

2、有一个地方你无法绕过去——如果你要修改当前用户的属性,而这个全局的当前用户不是当前Context产生的,你还非得从当前Context取出来,然后再修改;

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

 

IQueryable < TransactionInfo >  GetTransactionsByUserId( int  userId);
IQueryable
< TransactionInfo >  GetTransactionsByCompanyId( int  companyId);

因为这种设计实施之后,有时很可能就会出现只有userId的情况,那么这个时候即使UserInfo对象中其实也存在CompanyId的值,也还是要重新获取一遍UserInfo对象。为了简化这一过程,就可能会产生不同的获取形式。

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

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

 

Code
    partial 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
    /// 
    
/// 实现自动抛弃当前数据库上下文的模块
    
/// 
    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
    }

 

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

 

Code
        public 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;
        }

 

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

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

目录
相关文章
|
28天前
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:详细步骤与最佳实践指南ali01n.xinmi1009fan.com
随着Web开发技术的不断进步,ASP.NET已成为一种非常流行的Web应用程序开发框架。在ASP.NET项目中,我们经常需要与数据库进行交互,特别是SQL数据库。本文将详细介绍如何在ASP.NET项目中连接SQL数据库,并提供最佳实践指南以确保开发过程的稳定性和效率。一、准备工作在开始之前,请确保您
132 3
|
30天前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
50 11
|
27天前
|
开发框架 前端开发 API
C#/.NET/.NET Core优秀项目和框架2024年9月简报
C#/.NET/.NET Core优秀项目和框架2024年9月简报
|
1月前
|
SQL 开发框架 .NET
ASP连接SQL数据库:从基础到实践
随着互联网技术的快速发展,数据库与应用程序之间的连接成为了软件开发中的一项关键技术。ASP(ActiveServerPages)是一种在服务器端执行的脚本环境,它能够生成动态的网页内容。而SQL数据库则是一种关系型数据库管理系统,广泛应用于各类网站和应用程序的数据存储和管理。本文将详细介绍如何使用A
52 3
|
2月前
|
开发框架 .NET C#
VSCode开发.net项目时调试无效
【9月更文挑战第22天】在使用 VSCode 开发 .NET 项目时遇到调试问题,可从项目配置、调试配置、调试器安装、运行环境、日志和错误信息等方面排查。确认项目类型及文件配置,检查 `launch.json` 文件及配置项,确保调试器扩展已安装并启用,验证 .NET 运行时版本和环境变量,查看 VSCode 输出窗口和项目日志文件,检查权限及代码错误。若问题仍未解决,可查阅官方文档或社区论坛。
winform .net6 和 framework 的图表控件,为啥项目中不存在chart控件,该如何解决?
本文讨论了在基于.NET 6和.NET Framework的WinForms项目中添加图表控件的不同方法。由于.NET 6的WinForms项目默认不包含Chart控件,可以通过NuGet包管理器安装如ScottPlot等图表插件。而对于基于.NET Framework的WinForms项目,Chart控件是默认存在的,也可以通过NuGet安装额外的图表插件,例如LiveCharts。文中提供了通过NuGet添加图表控件的步骤和截图说明。
winform .net6 和 framework 的图表控件,为啥项目中不存在chart控件,该如何解决?
|
27天前
|
存储 消息中间件 前端开发
.NET常见的几种项目架构模式,你知道几种?
.NET常见的几种项目架构模式,你知道几种?
|
27天前
|
边缘计算 开发框架 人工智能
C#/.NET/.NET Core优秀项目和框架2024年8月简报
C#/.NET/.NET Core优秀项目和框架2024年8月简报
|
27天前
|
开发框架 .NET 开发工具
.NET 9 中 LINQ 新增的功能
.NET 9 中 LINQ 新增的功能
|
28天前
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:实现过程与关键细节解析an3.021-6232.com
随着互联网技术的快速发展,ASP.NET作为一种广泛使用的服务器端开发技术,其与数据库的交互操作成为了应用开发中的重要环节。本文将详细介绍在ASP.NET中如何连接SQL数据库,包括连接的基本概念、实现步骤、关键代码示例以及常见问题的解决方案。由于篇幅限制,本文不能保证达到完整的2000字,但会确保
下一篇
无影云桌面