程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> .NET實例教程 >> 如何在ASP.NET項目裡面正確使用Linq to Sql

如何在ASP.NET項目裡面正確使用Linq to Sql

編輯:.NET實例教程

.

老久不上來寫技術類的東西了,偶爾回歸一下吧。(其實,這篇文章8個月前寫了個大半,後來一直沒有時間去完善,再後來就因為各種原因給放下來了。)

Linq to Sql 用的人也應該有些吧,我在cnblogs上面看老趙寫的那幾篇文章(請看08年9月左右的文章),感覺也很有深度,有不少啟發。因此我也打算寫一點我自己的實踐經驗,希望也能同樣給大家一些有用的啟發吧。

我首先想要問一下大家,Linq to Sql有哪些很特別的地方?這個問題的答案肯定五花八門,我說一下我看到的一些問題吧。

首先,Linq to Sql的基礎之一是DataContext,而另外一個基礎,則是通過映射產生的實體類,以及這些實體類的Table<>對象。這個不是廢話嘛!我想很多人都應該知道這個最基本的知識,不過卻不見得有多少人真正注意到,或者認真思考一下這裡面的“機關”。不知道“機關”在哪裡,那麼就不可能寫出合適的代碼。比如說,在某個頁面裡面(N層結構沒有給弄好的情況下),或者在某個業務邏輯裡面(有N層結構),你的Linq to Sql的代碼是否是長這樣的?

以下為引用的內容:

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因為上下文已經拋棄了,因此是無法使用的,業務層不得不每一次都重新從數據庫讀取該用戶所屬公司的數據。

這兩種形式如下所示:

以下為引用的內容:

// 通過實體對象來存儲
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做一個擴展:

以下為引用的內容:

    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裡面配置好):

以下為引用的內容:

    /// <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
    }

接下來,我們只要在邏輯層這麼直接寫即可:

以下為引用的內容:

        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來獲得)。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved