LINQ to SQL使用教程

前些时间用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的替代品,也不能带来执行效率上的提高(用的不好反而会更低效)。这里还需要特别说明的是:

  1. 只能用于Microsoft SQL Server这套DBMS,2005版及2008版我都试过,没问题。Oracle?MySQL?没门;
  2. 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,这正是我们想要的。

这个例子说明了:

  1. 我们在调试的时候得关注下LINQ to SQL到底干了些什么
  2. 使用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你也就掌握差不多了……呃,我是说即便你再遇到什么问题,你也肯定有解决的思路了。

 

posted @ 2013-02-22 11:27  guogangj  阅读(19346)  评论(6编辑  收藏  举报