前些時間用LINQ to SQL做了一些項目,現在打算總結一下,幫助新手快速入門,並寫一些別的教程沒提到的東西。
一、LINQ to SQL和別的LINQ to XXX有什么關系?
二、延遲執行(Deferred Loading)
三、什么是LINQ to SQL?
四,看看LINQ to SQL到底干了些啥?——創建自己的工具類
五、創建一個基本查詢
六,大致掃一掃
1,WHERE
2,DISTINCT
3,AVG/COUNT/SUM/MIN/MAX
4,GROUP BY
5,CASE WHEN
6,INNER JOIN和OUTER JOIN
6.1 內連接
6.2 外連接
7,ORDER BY
8,EXISTS
9,WHERE IN
10,UNION ALL/UNION
11,Intersect/Except
12,Skip-Take
13,直接執行SQL語句查詢
14,INSERT
15,UPDATE
16,DELETE
17,First/FirstOrDefault/Single
18,字符串操作
七、查詢條件拼接
八、自動事務處理
九、關於自增的ID字段
十、關於默認值
總結
一、LINQ to SQL和別的LINQ to XXX有什么關系?
我們能接觸到的別的帶有“LINQ”字眼的東西有:LINQ to Object和LINQ to Entity Framework。它們之間的關系可以說:除了使用了相似的語法,就沒什么關系了。
LINQ to Object使用的命名空間是:System.Linq,而LINQ to SQL使用的命名空間是System.Data.Linq。
這是一個簡單的LINQ to Object的例子:
static IEnumerable<int> FindGreaterThan5(IEnumerable<int> list) { foreach (var i in list) { if (i >= 5) yield return i; } } static void Main(string[] args) { List<int> listTest = new List<int>{ 8, 2, 7, 9, 1, 5, 3, 4 }; //找出所有大於等於5的數 IEnumerable<int> result = FindGreaterThan5(listTest); foreach (var i in result) { Console.WriteLine(i); } }
其中FindGreaterThan5的代碼可以用LINQ to Object改為:
static IEnumerable<int> FindGreaterThan5(IEnumerable<int> list) { return list.Where(i => i >= 5); }
執行的效果是完全一樣的,但要注意一點:LINQ to Object只是簡化了代碼,並非提高了效率。而且,有時候把一個foreach語句寫成一大坨LINQ表達式,其可讀性也不好,所以究竟用還是不用,這個就看你的需要了。
二、延遲執行(Deferred Loading)
像上述例子的那種使用yield return的方式返回一個可枚舉類型的函數,都會被“延遲”執行,要證明這點很簡單,改一下上面的代碼:
static IEnumerable<int> FindGreaterThan5(IEnumerable<int> list) { foreach (var i in list) { if (i >= 5) yield return i; else { throw new Exception("你看不到這個異常"); } } } static void Main(string[] args) { List<int> listTest = new List<int>{ 8, 2, 7, 9, 1, 5, 3, 4 }; //找出所有大於等於5的數 IEnumerable<int> result = FindGreaterThan5(listTest); }
這個程序執行沒有任何問題,你看不到異常,因為FindGreaterThan5根本沒有被執行,它只有在返回結果被用到的時候才會真正去執行(這樣做的好處后面會提到)。如果你把foreach加上,改為:
static void Main(string[] args) { List<int> listTest = new List<int>{ 8, 2, 7, 9, 1, 5, 3, 4 }; //找出所有大於等於5的數 IEnumerable<int> result = FindGreaterThan5(listTest); foreach (var i in result) { Console.WriteLine(i); } }
那這個異常就會出現,這個地方是要十分小心的,假如你這么寫:
IEnumerable<int> result; try { result = FindGreaterThan5(listTest); } catch(Exception) //你捕捉不到異常的 { return; }
那是捕捉不到異常的,因為它實際上發生的地方是接下來的foreach處。解決方法有兩種,一是try foreach語句,另一是“再包一層”,創建一個“Roll”函數:
static IEnumerable<int> FindGreaterThan5Roll(IEnumerable<int> list) { return FindGreaterThan5(list); }
然后try這個函數。
三、什么是LINQ to SQL?
貌似講了一堆跟LINQ to SQL無關的東西,但我向你保證理解這些內容真的很重要!OK……終於到了其它一般的教程的那個開頭:什么是LINQ to SQL?
跟LINQ to Object一樣,LINQ to SQL能夠使得你對Microsoft SQL Server的訪問代碼變得簡潔,它是對ADO.net的封裝。所以它並非ADO.net的替代品,也不能帶來執行效率上的提高(用的不好反而會更低效)。這里還需要特別說明的是:
- 只能用於Microsoft SQL Server這套DBMS,2005版及2008版我都試過,沒問題。Oracle?MySQL?沒門;
- LINQ to SQL已經有了更強大和復雜的替代品,LINQ to Entity Framework,簡稱LINQ to EF或者Entity Framework,詳情請自行Google一下。
那為什么還用LINQ to SQL?一來它繼續長期有效(雖然微軟停止更新它了),二來夠用並且好用,而LINQ to EF則相對復雜。
使用LINQ to SQL其實仍然是使用ADO.net,只是LINQ to SQL幫助你生成各個查詢語句,不需要你手工來寫,這樣有什么好處?最大的好處就是:只要你的代碼編譯通過,那么就能生成正確的SQL語句,而不是報運行時錯誤,然后讓你去檢查SQL語句。
當然,好處不止這個,還有如:簡單,開發快捷,更靈活的where從句生成等。
四,看看LINQ to SQL到底干了些啥?——創建自己的工具類
前面說了,LINQ to SQL其實是“聰明”地幫你生成查詢語句,但你不能完全相信它,因為它有時候是“自作聰明”,所以你要在調試的時候看看它究竟干了些什么。我的方法是將它的生成的SQL語句打印到Debug窗口中,這個小技巧幫我找到了不少的問題,OK,這里我把我寫的這個小小的DataContext的幫助類貼出來:
public class DebugWriter : TextWriter { public override void WriteLine(string value) { Debug.WriteLine(value); } public override void WriteLine(string format, params object[] arg) { Debug.WriteLine(format, arg); } public override Encoding Encoding { get { return Encoding.UTF8; } } } public static class DataContextHelper { public static void InitForModification(this DataContext dc) { dc.DeferredLoadingEnabled = false; #if DEBUG dc.Log = new DebugWriter(); #endif } /// <summary> /// 如果一個表中的某一列引用到別的表,默認情況下LINQ to SQL會在遍歷搜索結果的時候 /// 動態地去獲取別的表的內容,這樣就可能產生大量的SQL查詢 /// 當把DeferredLoadingEnabled設置為false之后,LINQ to SQL則關閉這項功能,省去了大量的開銷 /// 事實上,我們更多的時候會用到LoadWith,直接生成一條聯表查詢 /// </summary> public static void InitForQuery(this DataContext dc) { dc.ObjectTrackingEnabled = false; dc.DeferredLoadingEnabled = false; #if DEBUG dc.Log = new DebugWriter(); #endif } public static DateTime GetDbDateTime(this DataContext dc) { try { return dc.ExecuteQuery<DateTime>("SELECT GETDATE()").Single(); } catch (Exception ex) { throw new Exception("Database error.", ex); //建議替換成你的自定義異常類型 } } }
如果你對C#的擴展方法語法不是很清楚,那么可以參考我的另一篇博文:http://www.cnblogs.com/guogangj/archive/2012/03/02/2376927.html
這個幫助類很有用,把它引入到你的工程的namespace去吧(在我的工程中,我把它放入一個公共類庫中,因為好幾個工程都要引用到,避免重復,DRY,OK?)
- InitForQuery - 用於查詢(注意看看我寫的那幾行注釋,后面會提到)
- InitForModification - 用於增刪改
- GetDbDateTime - 由於LINQ to SQL不直接支持GetDate函數,所以寫一個函數來獲取DBMS的時間
接下來我們還是用Northwind數據庫為例子,看看如何使用LINQ to SQL。(Northwind是一個小型數據庫例子,很多代碼都用它作為范例,在這里獲取它的創建腳本:http://northwinddatabase.codeplex.com/)
五、創建一個基本查詢
給你的工程add一個new item,叫Products.dbml,然后打開之,再如圖把Products和Categories兩張表拽進去。
然后:
static void Main(string[] args) { ProductsDataContext db = new ProductsDataContext(); db.Log = new DebugWriter(); var products = from p in db.Products select p; foreach (var product in products) { Console.WriteLine(product.ProductName+ " (" + product.Category.CategoryName + ")"); } }
這里也許你有個問題,為什么from寫在前面,而不是select,select寫在前面不是更符合SQL的習慣么?想想看,如果你能自己在5分鍾內想出來說明你比我聰明(^_^),OK,其實也跟技術本身關系不大,這樣做的原因完全是為了我們的開發環境的智能提示(intellisense),知道from什么了,后面也就有智能提示了,否則select什么?不知道。
F5,調試運行,看吧,這么一點點代碼,就能把所有商品名稱及其類型都給打印出來了,是不是很方便?——且慢!打開你的Debug窗口看看:
我的天啊,一個查詢就能完成的事情,為什么生成了這么多的SQL語句?這就是我前面所提到的DeferredLoadingEnabled這個選項,看我寫的那個幫助類的InitForQuery方法的注釋。避免這種情況的方法是關閉這個選項,並顯式告訴LINQ to SQL,你需要哪些關聯的加載內容,讓LINQ to SQL自動給你生成一個聯表查詢。我們來改進下:
static void Main(string[] args) { ProductsDataContext db = new ProductsDataContext(); db.InitForQuery(); DataLoadOptions ds = new DataLoadOptions(); ds.LoadWith<Product>(p => p.Category); db.LoadOptions = ds; var products = from p in db.Products select p; foreach (var product in products) { Console.WriteLine(product.ProductName+ " (" + product.Category.CategoryName + ")"); } }
注意看,這次用了我前面提供的擴展方法InitForQuery,還有一個DataloadOptions選項,告訴LINQ to SQL需要哪些額外信息,這里我們需要Category信息。好,看看Debug窗口,這次只有一個SQL語句:
SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued], [t2].[test], [t2].[CategoryID] AS [CategoryID2], [t2].[CategoryName], [t2].[Description], [t2].[Picture] FROM [dbo].[Products] AS [t0] LEFT OUTER JOIN ( SELECT 1 AS [test], [t1].[CategoryID], [t1].[CategoryName], [t1].[Description], [t1].[Picture] FROM [dbo].[Categories] AS [t1] ) AS [t2] ON [t2].[CategoryID] = [t0].[CategoryID]
LINQ to SQL聰明地為我們生成了一個left outer join,這正是我們想要的。
這個例子說明了:
- 我們在調試的時候得關注下LINQ to SQL到底干了些什么
- 使用LoadWith,而不要使用自動延遲加載
另外,如果你不需要Product的所有列的話,你可以這樣寫:
var products = from p in db.Products select new { ProductId = p.CategoryID, ProductName = p.ProductName, UnitPrice = p.UnitPrice };
這樣生成的SQL語句是不太一樣的,自己觀察下,但這樣生成出來的對象,是不能用於Insert和Update的,這個要注意一下。
六,大致掃一掃
寫一篇無微不至的教程並非本文目的,這個已經有了不少好的教程,所以接下來就稍微簡略一些,希望能對大家起到一定的拋磚引玉的作用。
1,WHERE
把1994年后雇佣的,或者Title中包含“經理”的員工選出來
var result = from e in db.Employees where e.HireDate >= new DateTime(1994, 1, 1) || e.Title.Contains("Manager") select e;
2,DISTINCT
獲取有交易的客戶ID(重復的去掉)
var result = (from o in db.Orders select new { CustomerID = o.CustomerID }).Distinct();
3,AVG/COUNT/SUM/MIN/MAX
取得產品價格的平均值
var result = db.Products.Select(p => p.UnitPrice).Average(); var result = db.Products.Average(p => p.UnitPrice); var result = (from p in db.Products select p.UnitPrice).Average();
從這也能看出寫法並不是唯一的。其它集合操作函數也是類似的。
4,GROUP BY
選出每一類的最貴產品、最便宜產品及產品均價
var result = from p in db.Products group p by p.CategoryID into g select new { g.Key, Amount = g.Count(), MaxPrice = g.Max(item => item.UnitPrice), MinPrice = g.Min(item=>item.UnitPrice), AveragePrice = g.Average(item=>item.UnitPrice) };
5,CASE WHEN
var q = from c in db.Customers select new { Name = c.ContactName, Address = new { City = c.City, Region = c.Region == null ? "Unknown" : c.Region } };
翻譯為:
SELECT [t0].[ContactName] AS [Name], [t0].[City], (CASE WHEN [t0].[Region] IS NULL THEN CONVERT(NVarChar(15),@p0) ELSE [t0].[Region] END) AS [Region] FROM [dbo].[Customers] AS [t0]
6,INNER JOIN和OUTER JOIN
想用LINQ to SQL直接寫聯表查詢是很麻煩的,不過這也是唯一一個相比直接用SQL來得更麻煩的地方。
6.1 內連接
var result = from d in db.Order_Details join o in db.Orders on d.OrderID equals o.OrderID select new {d.OrderID, d.ProductID, d.UnitPrice, o.RequiredDate};
翻譯為
SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t1].[RequiredDate] FROM [dbo].[Order Details] AS [t0] INNER JOIN [dbo].[Orders] AS [t1] ON [t0].[OrderID] = [t1].[OrderID]
另外一種更緊湊的寫法
var result = from d in db.Order_Details from o in db.Orders.Where(item=>item.OrderID==d.OrderID) select new { d.OrderID, d.ProductID, d.UnitPrice, o.RequiredDate };
翻譯為
SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t1].[RequiredDate] FROM [dbo].[Order Details] AS [t0], [dbo].[Orders] AS [t1] WHERE [t1].[OrderID] = [t0].[OrderID]
執行效果一樣的
6.2 外連接
var result = from d in db.Order_Details join o in db.Orders on d.OrderID equals o.OrderID into temp from p in temp.DefaultIfEmpty() select new {d.OrderID, d.ProductID, d.UnitPrice, p.RequiredDate};
另一種更緊湊的寫法是:
var result = from d in db.Order_Details from o in db.Orders.Where(item => item.OrderID == d.OrderID).DefaultIfEmpty() select new {d.OrderID, d.ProductID, d.UnitPrice, o.RequiredDate };
翻譯為
SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t1].[RequiredDate] AS [RequiredDate] FROM [dbo].[Order Details] AS [t0] LEFT OUTER JOIN [dbo].[Orders] AS [t1] ON [t1].[OrderID] = [t0].[OrderID]
7,ORDER BY
正序的情況
var result = from p in db.Products orderby p.UnitPrice select p;
逆序的情況
var result = from p in db.Products orderby p.UnitPrice descending select p;
也可以選擇多列排序
var result = from p in db.Products orderby p.UnitPrice, p.UnitsInStock select p;
上面的句子也可以這么寫,一樣的
var result = from p in db.Products.OrderBy(item => item.UnitPrice).ThenBy(item => item.UnitsInStock) select p;
8,EXISTS
選出沒有訂單的客戶
var result = from c in db.Customers where !c.Orders.Any() select c;
其實Any還可以帶條件參數哦。
9,WHERE IN
查看指定的幾個客戶的訂單
var result = ( from o in db.Orders where ( new string[] { "AROUT", "BOLID", "FISSA" }) .Contains(o.CustomerID);
10,UNION ALL/UNION
Concat不會去除重復項目,相當於SQL的Union All;而Union會去除重復項,相當於SQL的Union
看看有來自哪些國家的客戶和雇員
var q = ( from c in db.Customers select c.Country ).Union( from e in db.Employees select e.Country );
11,Intersect/Except (是用復合查詢加EXIST實現的)
var q = ( from c in db.Customers select c.Country ).Intersect( from e in db.Employees select e.Country );
查詢顧客和職員同在的國家,Interset其實是用一個復合查詢實現的。
var q = ( from c in db.Customers select c.Country ).Except( from e in db.Employees select e.Country );
去除顧客和職員同在的國家。
12,Skip-Take(是用ROW_NUMBER()函數實現的)
獲取從第21個產品開始的10個產品。
var q = ( from p in db.Products select p ).Skip(20).Take(10);
13,直接執行SQL語句查詢
如果發現查詢語句很難寫,(或者寫出來LINQ會傻乎乎地生成低效的多次執行)可以考慮直接使用SQL語句,但缺點就是失去了編譯器檢查的功能,並且得自己構建好一個類,用於存放數據(如果此類還沒有的話)。
IEnumerable<Employee> emps = db.ExecuteQuery<Employee>("select * from Employees");
PS:直接寫“*”可不太好
14,INSERT
注意:如果指定的ID存在,則會自動執行Update,而不是Insert(LINQ to SQL是不是很“智能”?都有些自作聰明了)
Region nwRegion = new Region() { RegionID = 32, RegionDescription = "Rainy" }; db.Regions.InsertOnSubmit(nwRegion); db.SubmitChanges();
15,UPDATE
真奇怪,update和insert居然不同,沒有一個專門的Update方法,而是直接取出數據庫的條目,然后修改其屬性,在SubmitChanges,當然,條目的類型必須是dbml自動幫我們生成的類型,不能是自定義的。這是簡單的Update的例子:
Customer cust = db.Customers.First(c => c.CustomerID == "ALFKI"); cust.ContactTitle = "Vice President"; db.SubmitChanges();
這是Update多條的例子:
var q = from p in db.Products where p.CategoryID == 1 select p; foreach (var p in q) { p.UnitPrice += 1.00M; } db.SubmitChanges();
16,DELETE
先選后刪
OrderDetail orderDetail = db.OrderDetails.First (c => c.OrderID == 10255 && c.ProductID == 36); db.OrderDetails.DeleteOnSubmit(orderDetail); db.SubmitChanges();
17,First/FirstOrDefault/Single(是用TOP實現的)
都是取出一條記錄,區別是:
- First – 至少有一條,否則拋異常
- FirstOrDefault – 如果一條都沒有,則返回默認值(對象的話默認值就是null)
- Single – 有且只有一條,否則拋異常
選出ID為1的雇員:
Employee theveryemp = db.Employees.Single(item => item.EmployeeID == 1);
這個語句會立即執行,如果獲取不到或者不止一條,則拋出異常。
18,字符串操作
以下這些LINQ中對字符串操作的部分都會被聰明地轉變為相關的SQL語句,而不是使用C#代碼來操作。具體會被轉換成什么,大家動手試試看。
Location = c.City + ", " + c.Country p.ProductName.Length < 10 c.ContactName.Contains("Anders") c.ContactName.IndexOf(" ") c.ContactName.StartsWith("Maria") c.ContactName.EndsWith("Anders") p.ProductName.Substring(3); e.HomePhone.Substring(6, 3) == "555" e.LastName.ToUpper() c.CategoryName.ToLower() e.HomePhone.Substring(0, 5).Trim() e.HomePhone.Insert(5, ":") e.HomePhone.Remove(9) e.HomePhone.Remove(0, 6) s.Country.Replace("UK", "United Kingdom")
七、查詢條件拼接
客戶端在查詢的時候往往會附帶一些查詢條件,例如商品名稱模糊查詢,日期范圍,商品類型范圍,當然還有分頁等等。我們通常把這些查詢條件封裝到一個對象中去,然后讓服務器來解釋這個對象並拼接SQL查詢語句。拼接SQL語句是極其容易出錯的事情,而且檢查起來還比較費勁,因為SQL語句寫起來並不像C#代碼那樣可以自動格式化。如今使用LINQ to SQL這些問題就不存在了。
var q = from p in db.Products select p; if(條件A) { q.Where(p=>p.XXX==A); } if(條件B) { q.Where(p=>p.YYY!=B); } if(條件C) { q.Where(p=>p.ZZZ.Contains(C)); } //……
回頭想想本文初所提到的延遲執行,到這里你應該知道為什么需要延遲執行了吧,就是為了方便你拼接這些LINQ表達式,如果每個select或者where都執行一次,那可是會帶來嚴重的性能問題的。
八、自動事務處理
也許你發現了,對於增刪改,都是在SubmitChanges的時候執行,而且一次SubmitChanges,能改多個表多條記錄,那事務在哪里?其實LINQ to SQL已經自動幫我們封裝好事務了,在執行的過程中,一旦有一步失敗,操作將會回滾,這個完全不需要我們擔心。
九、關於自增的ID字段
按設計慣例,每張表都應該有一個自增的ID字段,對於這個字段,其值總是由數據庫自動生成的,所以我們在Insert一行的時候,從來不需要指定其ID。例如,我們查看ProductID的屬性列表,有個叫Auto Generated Value的屬性,其值為True,即代表這個字段的值是DBMS自動生成的,你不需要指定。
那么我們插入了一條記錄之后,我們想取回這個自動生成的ID的值,怎么辦呢?按以前的做法是:
select scope_identity()
現在的做法是在SubmitChanges()之后直接通過插入的對象帶出這個自動生成的ID的值:
Category newCategory = new Category {CategoryName = "Fruit", Description = "Fruits..."}; db.Categories.InsertOnSubmit(newCategory); db.SubmitChanges(); Console.WriteLine(newCategory.CategoryID);
那現在有這么種情況,我要添加一個訂單,同時給這個訂單添加若干條明細,怎么辦?這個看起來有點難,難在哪里?你要添加明細,你就必須知道主檔的ID,但在SubmitChanges之前,你是拿不到主檔的ID的;如果你在Insert了主檔之后就Submit,那一旦在Insert明細的時候失敗,你就無法回滾了。
OK,其實這么想的話還是按照舊的思路,LINQ to SQL是一套ORM,我們應該直接指定其對象的關系,而不是去關心ID的值是多少,這是正確的做法:
Order newOrder = new Order { CustomerID = "ALFKI", ShipAddress = "Shanghai ..." }; Order_Detail newDetail1 = new Order_Detail { Discount = 1.0f, ProductID = 1, Quantity = 1, UnitPrice = 9.0M }; Order_Detail newDetail2 = new Order_Detail { Discount = 0.9f, ProductID = 2, Quantity = 2, UnitPrice = 5.0M }; newOrder.Order_Details.Add(newDetail1); newOrder.Order_Details.Add(newDetail2); db.Orders.InsertOnSubmit(newOrder); db.SubmitChanges();
十、關於默認值
數據庫的表中的很多字段都帶有默認值,前面提到的自增ID就是一個例子,但大多字段跟ID不同的是:ID是強制的並且一定是DBMS自動分配的,而這些帶默認值的字段則不一定強制,並且允許由用戶傳入值。如果把這些帶默認值的字段跟自增ID一樣,設置其“Auto Generated Value”屬性為True的話,我們就沒辦法設置它的值了,它的值總是由DBMS自動分配,而事實上,我們想要的結果是:當我沒有給值的時候使用默認值,當我有給值的時候,使用我的值。很不幸,LINQ to SQL做不到,我嘗試過很多種方法,結果很明確,就是做不到!這也許算是LINQ to SQL的一個缺陷吧。
所以,使用LINQ to SQL的話就把DBMS的默認值忽略掉吧,每次都手工把值傳進去好了……
總結
感謝你看完本文,本文肯定不是最全面的對LINQ to SQL的技術文章,但真心是本人實戰的總結,如果上面提到的內容你都理解,那LINQ to SQL你也就掌握差不多了……呃,我是說即便你再遇到什么問題,你也肯定有解決的思路了。