Ø  简介

C# 中与数据交互最常用的语句就是 LINQ 了,而 LINQ to SQL 是最直接与数据库打交道的语句,它可以根据 LINQ 语法生成对应的 SQL 语句,在数据库中去执行。本文主要研究什么样 LINQ 语句会生成什么样 SQL 语句,从而确保编写 LINQ 语句时,便知道该语句会执行什么样的 SQL 语句,得到什么结果,这是很有必要的。因为很多时候会考虑到 SQL 效率问题,和数据是否有误等问题。主要包括:

1.   插入数据

2.   判断记录是否存在

3.   左连接情况下,判断某字段是否为 null 的写法

4.   连接查询关联不同类型(string & int)字段

5.   关联表(一对多关系),一条语句查询主表与子表数据

6.   使用 IQueryable<T> 类型的变量,嵌入 LINQ 查询语法中

7.   常见的查询技巧

8.   使用对象集合进行连接查询

 

1.   插入数据

1)   LINQ 语句

DataContext.CustomerTodos.Add(entity);

DataContext.SaveChanges();

2)   生成 SQL

exec sp_executesql N'INSERT [dbo].[Crm_CustomerTodo]([CustomerId], [ProductId], [CreateTime], [TodoTime], [UpdateTime], [ExpireTime], [TodoType], [State], [Result], [Source], [VisitInfoId], [CppId], [AuditorId], [AuditorTime], [AuditorState], [AuditorDesc], [SalesUserId], [CouponItemNo])

VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, NULL, @10, NULL, NULL, NULL, NULL, NULL, @11)

SELECT [Id]

FROM [dbo].[Crm_CustomerTodo]

WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()',N'@0 bigint,@1 bigint,@2 datetime2(7),@3 datetime2(7),@4 datetime2(7),@5 datetime2(7),@6 int,@7 int,@8 int,@9 int,@10 int,@11 nvarchar(max) ',@0=80242,@1=0,@2='2018-10-19 13:52:07.8299645',@3='2018-10-19 13:52:07.8299645',@4='2018-10-19 13:52:07.8299645',@5='2018-10-28 23:59:59',@6=6,@7=1,@8=0,@9=1,@10=2,@11=N'4568'

 

2.   判断记录是否存在

1)   LINQ 语句

DataContext.CustomerTodos.Any(o => o.TodoType == todoType && o.CustomerId == model.CustomerId);

2)   生成 SQL

exec sp_executesql N'SELECT

    CASE WHEN ( EXISTS (SELECT

        1 AS [C1]

        FROM [dbo].[Crm_CustomerTodo] AS [Extent1]

        WHERE ([Extent1].[TodoType] = @p__linq__0) AND ([Extent1].[CustomerId] = @p__linq__1)

    )) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT

        1 AS [C1]

        FROM [dbo].[Crm_CustomerTodo] AS [Extent2]

        WHERE ([Extent2].[TodoType] = @p__linq__0) AND ([Extent2].[CustomerId] = @p__linq__1)

    )) THEN cast(0 as bit) END AS [C1]

    FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]',N'@p__linq__0 int,@p__linq__1 bigint',@p__linq__0=6,@p__linq__1=80242

 

3.   左连接情况下,判断某字段是否为 null 的写法

Ø  说明:UserInfo.CustomerId 关联 Customer.Id,为多对一的关系。但是 UserInfo.CustomerId 字段可能为 null

1)   LINQ 语句

var datas1 = (from t1 in DataContext.UserInfoes

                join t2 in DataContext.Customers on t1.CustomerId equals t2.Id into t12

                from t3 in t12.DefaultIfEmpty()

                where t1.id > 5000

                select new

                {

                    UserId = t1.id,

                    CustomerId1 = t1.CustomerId,

                    CustomerId2 = t3 == null ? 0 : t3.Id

                }).Take(3).ToArray();

2)   生成 SQL

SELECT

    [Limit1].[C1] AS [C1],

    [Limit1].[id] AS [id],

    [Limit1].[CustomerId] AS [CustomerId],

    [Limit1].[C2] AS [C2]

    FROM ( SELECT TOP (3)

        [Extent1].[id] AS [id],

        [Extent1].[CustomerId] AS [CustomerId],

        1 AS [C1],

        CASE WHEN ([Extent2].[Id] IS NULL) THEN cast(0 as bigint) ELSE [Extent2].[Id] END AS [C2]

        FROM  [dbo].[UserInfo] AS [Extent1]

        LEFT OUTER JOIN [dbo].[Customer] AS [Extent2] ON [Extent1].[CustomerId] = [Extent2].[Id]

        WHERE [Extent1].[id] > 5000

    )  AS [Limit1]

3)   执行结果

深入理解 LINQ to SQL 生成的 SQL 语句

4)   分析

1.   这种写法其实看起来很怪异,为什么要用一个实体去判断是否被关联呢(这是同事善用的而一种写法,哈哈)?

2.   可见,在生成的 SQL 中使用了 CASE 做了一下判断,使用右表的主键 Id,如果为 null,就输出默认值,否则正常输出。

3.   其实,我们还可以使用可空类型接受右表的字段,可以省去 CASE 子句的判断。

 

4.   连接查询关联不同类型(string & int)字段

Ø  说明:在一些非正常数据表的设计中,可能出现外键字段与主键字段类型不一致的情况,强制关联就可能编写语句:

1)   LINQ 语句

var datas = (from t1 in dbContext.Orders

                join t3 in dbContext.Cities on t1.CityId equals t3.Id + string.Empty

                where t1.OrderNum != "O123321"

                orderby t1.Id descending

                select new

                {

                    t1.OrderNum,

                    t3.CityName

                }).Take(10).ToArray();

2)   生成 SQL

exec sp_executesql N'SELECT TOP (10)

    [Project1].[Id1] AS [Id],

    [Project1].[OrderNum] AS [OrderNum],

    [Project1].[CityName] AS [CityName]

    FROM ( SELECT

        [Extent1].[Id] AS [Id],

        [Extent1].[OrderNum] AS [OrderNum],

        [Extent2].[Id] AS [Id1],

        [Extent2].[CityName] AS [CityName]

        FROM  [dbo].[Orders] AS [Extent1]

        INNER JOIN [dbo].[Sys_Cities] AS [Extent2] ON ([Extent1].[CityId] = ( CAST( [Extent2].[Id] AS nvarchar(max)) + CASE WHEN (@p__linq__0 IS NULL) THEN N'''' ELSE @p__linq__0 END)) OR (([Extent1].[CityId] IS NULL) AND ( CAST( [Extent2].[Id] AS nvarchar(max)) + CASE WHEN (@p__linq__0 IS NULL) THEN N'''' ELSE @p__linq__0 END IS NULL))

        WHERE  NOT ((N''O123321'' = [Extent1].[OrderNum]) AND ([Extent1].[OrderNum] IS NOT NULL))

    )  AS [Project1]

    ORDER BY [Project1].[Id] DESC',N'@p__linq__0 nvarchar(4000)',@p__linq__0=N''

3)   分析

1.   以上的 LINQ 语句在某些情况下是会抛异常的,System.NotSupportedException:“无法将类型“System.Int32”强制转换为类型“System.Object”。LINQ to Entities 仅支持强制转换 EDM 基元或枚举类型。”,因为本人测试时,相同的环境下,一个项目中可以正常执行,而另一个则会报错,原因不详!

2.   可见,外键字段(CityIdvarchar 类型,而主键(Id)字段为 int 类型,为了强制关联将 int 类型强制转为 varchar 类型,在生成的代码中也是如此。

3.   这样一来执行的 SQL 语句变得非常复杂,所以在实际的开发中,千万不能这样去设计数据表的结构!这里仅仅是为了演示这种非法设计的导致的后果。

 

5.   关联表(一对多关系),一条语句查询主表与子表数据

Ø  说明:我们经常会接触多表查询,比如一对多、多对多的查询。如果我们需要一条语句查询出主表与子表的数据,比如以下场景 Customer UserInfo 是一对多的关系,我们就可以编写下面的语句:

1)   LINQ 语句

说明:查询客户 Id 4535749667的客户与用户记录

var dataList = (from t1 in DataContext.Customers

                where t1.Id == 45357 || t1.Id == 49667

                select new CustomerModel()

                {

                    Id = (int)t1.Id,

                    Name = t1.Name,

                    UserList = (from d1 in DataContext.UserInfoes

                                where d1.CustomerId == t1.Id

                                select new CustomerModel.UserInfoModel()

                                {

                                    Id = d1.id,

                                    UserName = d1.userName

                                }).ToList()

                }).ToList();

2)   生成 SQL

SELECT

    [Project1].[Id] AS [Id],

    [Project1].[C1] AS [C1],

    [Project1].[C2] AS [C2],

    [Project1].[Name] AS [Name],

    [Project1].[C3] AS [C3],

    [Project1].[Id1] AS [Id1],

    [Project1].[userName] AS [userName]

    FROM ( SELECT

        [Extent1].[Id] AS [Id],

        [Extent1].[Name] AS [Name],

        1 AS [C1],

         CAST( [Extent1].[Id] AS int) AS [C2],

        [Extent2].[id] AS [Id1],

        [Extent2].[userName] AS [userName],

        CASE WHEN ([Extent2].[id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C3]

        FROM  [dbo].[Customer] AS [Extent1]

        LEFT OUTER JOIN [dbo].[UserInfo] AS [Extent2] ON [Extent2].[CustomerId] = [Extent1].[Id]

        WHERE [Extent1].[Id] IN (45357,49667)

    )  AS [Project1]

    ORDER BY [Project1].[Id] ASC, [Project1].[C3] ASC

3)   执行结果:

1.   数据库

深入理解 LINQ to SQL 生成的 SQL 语句

2.   程序

深入理解 LINQ to SQL 生成的 SQL 语句

4)   分析

1.   可见,生成的 SQL 语句转换成了左连接,将主表与子表中所需的字段一起查询,在返回的数据集中,应该是 EF 帮我们以主表字段分组,并获取子表结果集的操作。是不是 EF 还是挺强大,帮我们省去了很多代码!?

2.   以上语法虽然看上去不知道 LINQ 是如何处理的,但 LINQ 确实是支持的,而且比较简单好理解。

3.   我们除了使用以上语法,也可以类似的直接写左连接在程序中进行分组,再取明细结果集的方式。(其实与以上执行的 SQL 语句是一样的,这个根据自己的习惯方式决定)

4.   另外还有一种方式,先查询主表,在遍历或包含去查询子表(强烈不推荐该方式)。

 

6.   使用 IQueryable<T> 类型的变量,嵌入 LINQ 查询语法中

1)   LINQ 语句

var allCustomers = (from d1 in DataContext.Customers

                    select new

                    {

                        CustmerId = d1.Id,

                        CityId = d1.CityID,

                        CustomerName = d1.Name

                    });

var datas = (from d1 in allCustomers

                join d3 in DataContext.CustomerVisitInfoes on d1.CustmerId equals d3.CustomerId

                where 1 == 1

                && (d3.VisitTime >= st && d3.VisitTime <= et)

                && cityId == d1.CityId

                select new

                {

                    d1.CustmerId,

                    d1.CustomerName,

                    d3.EmployeeId

                }).ToArray();

2)   生成 SQL

exec sp_executesql N'SELECT

    [Extent2].[SaleUserId] AS [SaleUserId],

    [Extent1].[Id] AS [Id],

    [Extent1].[Name] AS [Name]

    FROM  [dbo].[Customer] AS [Extent1]

    INNER JOIN [dbo].[CustomerVisitInfo] AS [Extent2] ON [Extent1].[Id] = [Extent2].[CustomerId]

    WHERE ([Extent2].[VisitTime] >= @p__linq__0) AND ([Extent2].[VisitTime] <= @p__linq__1) AND (@p__linq__2 = [Extent1].[CityID])',N'@p__linq__0 datetime2(7),@p__linq__1 datetime2(7),@p__linq__2 bigint',@p__linq__0='2018-07-01 00:00:00',@p__linq__1='2018-07-31 23:59:59',@p__linq__2=73

3)   执行结果

深入理解 LINQ to SQL 生成的 SQL 语句

4)   分析

1.   首先,我们定义了一个 IQueryable<T> 类型的变量,然后在使用这个变量作为数据源,进行关联查询。可见,在生成的 SQL 中直接进行了内连接查询,并输出相关字段。

2.   这种写法主要是可以方便多次查询,可以多次利用这个 allCustomers 变量进行查询,从而可以省去相同的代码。当然,这样的 IQueryable<T> 变量也可以写的比较复杂,作用就显得比较明显了。

3.   但是这种嵌套不必嵌套太多层,不然影响可读性,不便于分析和理解。

 

7.   常见的查询技巧

1)   遍历查询 IQueryable<T> 对象

1.   LINQ 语句

var query1 = dbContext.Grades.Select(o => o);

foreach (var item in query1)

{

    Console.WriteLine("获取记录:{0}", item.GradeId);

}

2.   生成 SQL

SELECT

    [Extent1].[GradeId] AS [GradeId],

    [Extent1].[GradeName] AS [GradeName],

    [Extent1].[Remark] AS [Remark]

    FROM [dbo].[Grade] AS [Extent1]

3.   分析

1)   一次性生成 SQL,再遍历结果集。

 

2)   根据不同条件生成对应 SQL

1.   LINQ 语句

var query2 = dbContext.Grades.Select(o => o);

for (int i = 1; i <= 3; i++)

{

    var entity = query2.Where(o => o.GradeId == i).FirstOrDefault();

    Console.WriteLine("获取记录:{0}", (entity != null ? entity.GradeId : 0));

}

2.   生成 SQL(类似的3SQL

exec sp_executesql N'SELECT TOP (1)

    [Extent1].[GradeId] AS [GradeId],

    [Extent1].[GradeName] AS [GradeName],

    [Extent1].[Remark] AS [Remark]

    FROM [dbo].[Grade] AS [Extent1]

    WHERE ([Extent1].[GradeId] = @p__linq__0) AND (@p__linq__0 IS NOT NULL)',N'@p__linq__0 int',@p__linq__0=1

3.   分析

1)   每次遍历,都会生成不同的SQL,并查询数据库。

2)   通常情况下,不建议这样去遍历查询,可以先查询出所需的记录,再进行遍历。

 

3)   根据不同条件生成对应 SQL

1.   LINQ 语句

var query3 = (from t1 in dbContext.Grades

                where t1.GradeName.Contains("年级")

                orderby t1.GradeId descending

                select t1);

for (int i = 1; i <= 3; i++)

{

    var entity = query3.Where(o => o.GradeId == i).FirstOrDefault();

    Console.WriteLine("获取记录:{0}", (entity != null ? entity.GradeId : 0));}

2.   生成 SQL(类似的3SQL

exec sp_executesql N'SELECT TOP (1)

    [Project1].[GradeId] AS [GradeId],

    [Project1].[GradeName] AS [GradeName],

    [Project1].[Remark] AS [Remark]

    FROM ( SELECT

        [Extent1].[GradeId] AS [GradeId],

        [Extent1].[GradeName] AS [GradeName],

        [Extent1].[Remark] AS [Remark]

        FROM [dbo].[Grade] AS [Extent1]

        WHERE ([Extent1].[GradeName] LIKE N''%年级%'') AND ([Extent1].[GradeId] = @p__linq__0) AND (@p__linq__0 IS NOT NULL)

    )  AS [Project1]

    ORDER BY [Project1].[GradeId] DESC',N'@p__linq__0 int',@p__linq__0=1

3.   分析

1)   每次遍历,都会生成不同的SQL,并查询数据库。

2)   并将条件以 AND(与)进行追加。

3)   通常情况下,不建议这样去遍历查询,可以先查询出所需的记录,再进行遍历。

 

8.   使用对象集合进行连接查询

1)   LINQ 语句

Class1[] objects = new Class1[]

{

    new Class1 { Id = 1, Name = "Name1" },

    new Class1 { Id = 2, Name = "Name2" },

    new Class1 { Id = 3, Name = "Name3" }

};

var query1 = (from t1 in objects

                join t3 in dbContext.Grades on t1.Id equals t3.GradeId

                where t1.Name.Contains("2") && t3.GradeName.Contains("")

                select new { t3.GradeId, t1.Name, t3.GradeName });

foreach (var item in query1)

{

    Console.WriteLine("获取记录:{0},{1},{2}", item.GradeId, item.Name, item.GradeName);

}

2)   生成 SQL

SELECT

    [Extent1].[GradeId] AS [GradeId],

    [Extent1].[GradeName] AS [GradeName],

    [Extent1].[Remark] AS [Remark]

    FROM [dbo].[Grade] AS [Extent1]

3)   执行结果

深入理解 LINQ to SQL 生成的 SQL 语句

4)   分析

1.   可见,以对象集合关联查询,生成的查询 SQL 中并没有任何 where 条件。

2.   首先查询所有的“右表数据”,再在程序中进行过滤。

3.   注意:这种关联查询并不是一种标准查询,并且对象集合必须为左表(t1的位置)。否则将抛出异常:System.NotSupportedException,无法创建“EFDBFirst6_0.Class1”类型的常量值。此上下文仅支持基元类型或枚举类型。

4.   最后,不推荐使用该方式连接查询,影响正常思维和效率。

相关文章: