在Entity Framework中使用left outer join 想必你们很多人都用过,但是为什么要那么用,我们一般也没去深究,这里稍微做一下探讨,不对的希望大家指正。
先准备我们的数据对象,这里使用最简单的one to many ,一个Category 对应多个Product,代码如下:
class Category { public int CategoryId { get; set; } public string CategoryName { get; set; } } class Product { public int ProductId { get; set; } public string ProductName { get; set; } public int CategoryId { get; set; } }
查询代码:
var categories = new List<Category>() { new Category() {CategoryId = 1, CategoryName = "Category-1"}, new Category() {CategoryId = 2, CategoryName = "Category-2"}, new Category() {CategoryId = 3, CategoryName = "Category-3"} }; var products = new List<Product>() { new Product() {ProductId = 1, CategoryId = 1, ProductName = "product c1-p1"}, new Product() {ProductId = 2, CategoryId = 1, ProductName = "product c1-p2"}, new Product() {ProductId = 3, CategoryId = 2, ProductName = "product c2-p3"}, new Product() {ProductId = 4, CategoryId = 2, ProductName = "product c2-p4"}, //new Product() {ProductId = 5, CategoryId = 3, ProductName = "product c3-p5"} }; var result = from c in categories join p in products on c.CategoryId equals p.CategoryId into g from item in g.DefaultIfEmpty() select c;
这个地方,我们一般有3个地方不明白,
1. g.DefaultIfEmplty()这个函数是什么意思?为什么要使用它?
2. into g,这个g到底是一个什么格式的数据?
3. 使用Into 和不使用 into 有什么区别?为什么要使用into?
接下来我通过一些代码,来解答以上三个问题:
1. DefaultIfEmplty 这个函数是什么意思?为什么要使用它?
首先我们从字面上就可以看出这个函数大体是干啥的,如果为空就返回默认值,然后去看看微软官方是怎么解释这个函数的,
方法的签名:
public static IEnumerable<TSource> DefaultIfEmpty<TSource>( this IEnumerable<TSource> source )这个函数的解释:Returns the elements of the specified sequence or the type parameter‘s default value in a singleton collection if the sequence is empty.返回指定序列的元素;如果序列为空,则返回单一实例集合中的类型参数的默认值。我相信很多人看到这个解释后,都一头雾水,本来有点明白的,结果一看反而不明白了,先看这个方法的第一个参数有一个前缀this, 就知道这是一个扩展方法了,而且是扩展了所有实现了IEnumerabe<TSource>接口的类,说白了就是对序列元素的一个扩展(数组,list, 等我们经常使用的那些),看例子:var categories = new List<Category>(); var defaultCategories = categories.DefaultIfEmpty(); Console.WriteLine("category‘s count: {0}",categories.Count); Console.WriteLine("defaultCategories‘s count: {0}",defaultCategories.Count()); Console.WriteLine("loop defaultCategories--------------"); foreach (var category in defaultCategories) { if (category == null) Console.WriteLine("category is null"); else Console.WriteLine(category.CategoryId); } /* This code produces the following output: category‘s count: 0 defaultCategories‘s count: 1 loop defaultCategories-------------- category is null */
我们可以看到,categories有0个元素,但是defaultCategories有1个元素,然后循环defaultCategories发现,它有一个null的元素,看到这里这个方法的作用,我相信都看的非常清楚了。2. into g,这个g到底是一个什么格式的数据?废话不多说,看代码:var result = (from c in categories join p in products on c.CategoryId equals p.CategoryId into g select g).FirstOrDefault(); foreach (var ret in result) { Console.WriteLine(ret.ProductName); } /* * This code produces the following output: * product c1-p1 * product c1-p2 */我们可以看到g 实际上是一个List<Product>的集合。
3. 使用Into 和不使用 into 有什么区别?为什么要使用into?
当不使用 INTO 时查询出来的数据是一个平面二维的数据表,类似于这样的
Category-1 product c1-p1 Category-1 product c1-p2 Category-2 product c2-p3 Category-2 product c2-p3 用代码验证一下:
var result = from c in categories join p in products on c.CategoryId equals p.CategoryId select new {c.CategoryName, p.ProductName}; foreach (var ret in result) { Console.WriteLine("{0}----{1}",ret.CategoryName,ret.ProductName); } /* * This code produces the following output: * Category-1----product c1-p1 * Category-1----product c1-p2 * Category-2----product c2-p3 * Category-2----product c2-p4 */
当使用INTO 是数据查询出来是一个类似字典的集合<Category, List<Product>>这样的一个集合,本来想弄个表格来展示的,折腾了半天怎么也不能合并行,只好作罢改用文字描述吧:Category-1, Products:{product 1, product2};Category-2, Products: {product 3, product 4};大体就是上面这个样子了,同样我们整段代码瞧瞧:var result = from c in categories join p in products on c.CategoryId equals p.CategoryId into g select new {c, g}; foreach (var ret in result) { Console.Write("{0}: ",ret.c.CategoryName); foreach (var product in ret.g) { Console.Write(" {0}",product.ProductName); } Console.WriteLine(); } /* * This code produces the following output: * Category-1: product c1-p1 product c1-p2 * Category-2: product c2-p3 product c2-p4 */OK,三个问题,我们都明白了,现在开始说正事,先看看inner join, 也就是不用DefaultIfEmpty方法的时候Inner Joinvar result = from c in categories join p in products on c.CategoryId equals p.CategoryId into g from d in g select new {CName= c.CategoryName, PName= d.ProductName}; foreach (var ret in result) { Console.WriteLine("{0}---{1}",ret.CName,ret.PName); } /* * This code produces the following output: * Category-1---product c1-p1 * Category-1---product c1-p2 * Category-2---product c2-p3 * Category-2---product c2-p4 */
Left Joinvar result = from c in categories join p in products on c.CategoryId equals p.CategoryId into g from d in g.DefaultIfEmpty() select new {CName= c.CategoryName, PName= d==null? "no products" : d.ProductName}; foreach (var ret in result) { Console.WriteLine("{0}---{1}",ret.CName,ret.PName); } /* * This code produces the following output: * Category-1---product c1-p1 * Category-1---product c1-p2 * Category-2---product c2-p3 * Category-2---product c2-p4 * Category-3---no products */
看到不同之处了没,有三个个地方不同,第一个:使用inner join的时候,我们直接使用 [from d in g], 但是在left join时用的是[from d in g.DefaultIfEmpty()],第二个:我们在left join 的select 子句中,去判断了d是否等于null:[select new {CName= c.CategoryName, PName= d==null? "no products" : d.ProductName}; ] .为什么要这么做,就是应为如果当一个category没有找到匹配的product集合时,我们通过DefaultIfEmpty返回了一个含有一个null 元素的List<Product> 集合,然后我们[from d in g.DefaultIfEmpty()], 那么这个d就有可能为Null了,这就是我们为什么要去判断d 是否为空的原因了。第三个:这个不同就是输出信息的不同了,最后多了个[Category-3---no products],从这里我们终于看到了有一个category 没有对应的产品,这就是left outer join
终于把Linq to object 的Left Join说明白了,但是还是跟我们的Entity Framework没一根毛的关系,我们创建一个Category, Product表,然后通过DB first来创建我们的entity 实体,
下面是建表语句:
CREATE TABLE Category (CategoryId INT PRIMARY KEY IDENTITY(1,1), CategoryName VARCHAR(255)); CREATE TABLE Product (ProductId INT PRIMARY KEY IDENTITY(1,1), ProductName VARCHAR(255), CategoryId INT); ALTER TABLE Product ADD CONSTRAINT FK_Category_Product FOREIGN KEY(CategoryId) REFERENCES Category(CategoryId); INSERT INTO Category VALUES(‘Category-1‘); INSERT INTO Category VALUES(‘Category-2‘); INSERT INTO Category VALUES(‘Category-3‘); INSERT INTO Product(ProductName, CategoryId) VALUES(‘Product c1-p1‘, 1); INSERT INTO Product(ProductName, CategoryId) VALUES(‘Product c1-p2‘, 1); INSERT INTO Product(ProductName, CategoryId) VALUES(‘Product c1-p3‘, 2); INSERT INTO Product(ProductName, CategoryId) VALUES(‘Product c1-p4‘, 2); GO
怎么通过db first 建立entity 实体,这里不讲,准备工作做好后,来看下面代码:
using (var ctx = new EFInActionEntities()) { var result = from c in ctx.MyCategories join p in ctx.MyProducts on c.CategoryId equals p.CategoryId into g from d in g.DefaultIfEmpty() select new {CName = c.CategoryName, PName = d == null ? "No Products" : d.ProductName}; foreach (var ret in result) { Console.WriteLine("{0}--{1}", ret.CName,ret.PName); } } /* * Category-1--Product c1-p1 * Category-1--Product c1-p2 * Category-2--Product c1-p3 * Category-2--Product c1-p4 * Category-3--No Products */
可以看到,输出的结果,和Linq to Object 时是一样的,看看生成的SQL吧:
SELECT [Extent1].[CategoryId] AS [CategoryId], [Extent1].[CategoryName] AS [CategoryName], CASE WHEN ([Extent2].[ProductId] IS NULL) THEN N‘No Products‘ ELSE [Extent2].[ProductName] END AS [C1] FROM [dbo].[Category] AS [Extent1] LEFT OUTER JOIN [dbo].[Product] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[CategoryId]
差不多就这些了,不知道你们明白了没有,关键点就是要搞明白 into 语句, 和into 语句得到的数据格式!
Entity Framwork——Left Join