Entity Framework 实体数据模型——LINQ 技巧之一

 一、LINQ 与 EF

  从前面的讨论,可以看到 Code First 模型比 EF 设计模式要精简很多,在实际项目开发中,也建议直接采用 Code First 模式。

  因此,接下来的示例我们都采用 Code First 模式来展开讨论。

  1、Select 语句

    首先,创建 LINQDemo 项目,然后添加 ADO.Net 实体数据模型,选择 Code First 模式,选择事先创建好的数据库。

    接下来,在 Main 方法中写一个 LINQ 的 Select 语句:

     using (var context = new StudentInfoModel())
     {
         var stus = from stu in context.Student select stu;
         foreach (var stu in stus)
         {
             Console.WriteLine(stu.Name);
         }
     }
     Console.ReadKey();

     

    在 LINQ 中,Select 语句负责查询结果,除了查询完整的对象、单个或特定的属性成员外,同时它还支持功能强大的类型转换功能。

    通过该功能,我们可以借助 LINQ 查询选择的对象集合来创建一组全新的类型(类型转换):

     using (var context = new StudentInfoModel())
     {
         var stus = from stu in context.Student select stu;
 
         foreach (var stu in stus)
         {
             Console.WriteLine($"姓名:{stu.Name} \t年龄:{stu.Age} \t爱好:{stu.Hobby}");
         }
     }
     Console.ReadKey();

    a、类型转换

    基于上面输出的查询结果,我们可以进行指定属性查询:

    新建一个 SelectResult.cs 类。

     using System;
     using System.Linq;

     namespace LINQDemo
     {
         class SelectResult
         {
             public void GetResult()
             {
                 using (var context = new StudentInfoModel())
                 {
                     var stus = from stu in context.Student
                                select new
                                {
                                    stu.Name,
                                    stu.Age,
                                    stu.Hobby
                                };
                     foreach (var stu in stus)
                     {
                         Console.WriteLine($"姓名:{stu.Name} \t年龄:{stu.Age} \t爱好:{stu.Hobby}");
                     }
                 }
             }
         }
     }

    然后回到主程序(Main 方法)中调用该方法:

    

      

    在 Select 语句中获取了指定的 Name、Age、Hobby属性之后,通过 new 关键字创建了新的返回对象。

     

    如你所见,只要通过适当的 LINQ 语句,我们就可以取出特定的数据。

    我们还可以为属性指定标识名:

    

    Select 语句中,在创建的新对象中逐一为属性指定了映射名,同时将原来的年龄减去了 1 岁。

    可以看出,Select 语句除了选择数据外,还进一步转换了结果,创建了新的结果对象类型。创建对象的过程就是所谓的映射。

    在实际开发中,针对特定的数据处理,我们会定义实体类,以支持更进一步的强类型数据操作。

    所以,接下来我们需要定义一个实体类对象,用以存储查询结果及数据处理:

     using System;
     using System.Linq;

     namespace LINQDemo
     {
         class SelectResult
         {
             public void GetResult()
             {
                 using (var context = new StudentInfoModel())
                 {
                     IQueryable<ResultObj> stus = from stu in context.Student
                                                  select new ResultObj()
                                                  {
                                                      SID = stu.Id,
                                                      StuID = "2020" + stu.Id.ToString(),
                                                      StuName = stu.Name,
                                                      StuAge = stu.Age - 1,
                                                      StuHobby = stu.Hobby
                                                  };
                     foreach (var stu in stus)
                     {
                         Console.WriteLine($"ID:{stu.SID} \t编号:{stu.StuID} \t姓名:{stu.StuName} \t年龄:{stu.StuAge} \t爱好:{stu.StuHobby}");
                     }
                 }
             }

             class ResultObj
             {
                 public int? SID { get; set; }
                 public string StuID { get; set; }
                 public string StuName { get; set; }
                 public int? StuAge { get; set; }
                 public string StuHobby { get; set; }
             }
         }
     }

    

    这么看来,感觉 Select 语句有点儿复杂,有什么方法可以简化么?

    当然,我们可以把 new 对象的逻辑单独的放到独立的方法(或函数)中:

     public void GetResult()
     {
         using (var context = new StudentInfoModel())
         {
             IEnumerable<ResultObj> stus = from stu in context.Student.ToList()
                                           select ConvertToResultObj(stu);
             foreach (var stu in stus)
             {
                 Console.WriteLine($"ID:{stu.SID} \t编号:{stu.StuID} \t姓名:{stu.StuName} \t年龄:{stu.StuAge} \t爱好:{stu.StuHobby}");
             }
         }
     }

     ResultObj ConvertToResultObj(Student stu)
     {
         return new ResultObj()
         {
             SID = stu.Id,
             StuID = "2020" + stu.Id.ToString(),
             StuName = stu.Name,
             StuAge = stu.Age - 1,
             StuHobby = stu.Hobby
         };
     }

 

  2、多重 from 与 SelectMany

    了解了 Select 语句后,再来学习 SelectMany 就会容易许多,因为 SelectMany 只是搭配了多重 from 子句的 Select 方法。

    // 第二个参数 Func<TSource, IEnumerable<TResult>> selector 
    // IEnumerable<TResult> 这个表示将 IEnumerable 对象当做参数传入做进一步处理,构成了多重 from 运算。
public static IEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> source,
                                              Func<TSource, IEnumerable<TResult>> selector);

    在学习 SelectMany 方法之前,先来看两个多重 from 语句的例子:

     using System;
     using System.Linq;
 
     namespace LINQDemo
     {
         class Program
         {
             static void Main(string[] args)
             {
                 string[] weekMonth = { "January,February,March,April,May,June,July,August,September,October,November,December",
                                        "Monday,Tusday,Wednesday,Thursday,Friday,Saturday,Sunday" };
 
                 var enumMonth = from wm in weekMonth
                                 from daywm in wm.Split(',')
                                 select new
                                 {
                                     week = daywm.Contains("day") ? daywm.Substring(0, 3):"",
                                     month = !daywm.Contains("day") ? daywm.Substring(0, 3):"",
                                 };

                 string month = "星期的缩写:\n";
                 foreach (var mon in enumMonth)
                 {
                     if(mon.week!="")
                         month += mon.week + "\n";
                 }
                 Console.WriteLine(month);
 
                 month = "月份的缩写:\n";
                 foreach (var mon in enumMonth)
                 {
   month += mon.month + "\n";
} Console.WriteLine(month); Console.ReadKey(); } } }

    

    上面的例子是将数组中的数据提取成缩写形式并输出!用了嵌套 from 语句。

    第一个 from 语句是将月份和星期提取出来,第二个 from 将提取出来的对象通过 ‘,’ 将其分解,然后再挨个处理数组中的数据;

    最后返回一个由 week 和 month 属性组成的匿名对象。像这种就是嵌套查询了。

    接下来,咱们用 LINQ 结合 EF 实体数据结构,来看看多重 from 是怎么操作的?

    首先,需要添加一个年级表,学生表能添加一个关联年级表的ID,表结果如下:

    Student 表:

    

    ClassGrade 表:

    

    年级表和学生表 是一对多的关系。

    接下来我们尝试用 LINQ 语句,将学生用年级来分组并输出:

     using System;
     using System.Linq;
 
     namespace LINQDemo
     {
         class SelectMany
         {
             public void ClassToStu()
             {
                 using(var context = new StudentInfoModel())
                 {
                     var stus = from classes in context.CGrade
                                from stu in classes.Students
                                orderby classes.GradeNum,classes.ClassNum
                                select new
                                {
                                    classes.GradeNum,
                                    classes.ClassNum,
                                    stu.Name
                                };
 
                     foreach (var stu in stus)
                     {
                         Console.WriteLine($"{stu.GradeNum}年级{stu.ClassNum}班:\t{stu.Name}");
                     }
                 }
             }
         }
     }

    

    我们通过 LINQ 实现了归组链表查询,接下来看看如何把上面的 LINQ 语句通过 SelectMany() 方法来实现:

     using System;
     using System.Linq;
 
     namespace LINQDemo
     {
         class SelectMany
         {
             public void ClassToStu()
             {
                 using (var context = new StudentInfoModel())
                 {
                     var students = context.CGrade.SelectMany(
                         s => s.Students.Select(
                             st => new
                             {
                                 s.GradeNum,
                                 s.ClassNum,
                                 st.Name
                             })).OrderBy(s => s.GradeNum).ThenBy(s => s.ClassNum);

                     foreach (var stu in students)
                     {
                         Console.WriteLine($"{stu.GradeNum}年级{stu.ClassNum}班:\t{stu.Name}");
                     }
                 }
             }
         }
     }

    我们还可以通过 where 方法来过滤出我们想要查询的数据,比如 我想查出属于一年级的同学:

     var stus = from classes in context.CGrade where classes.GradeNum == 1
                 from stu in classes.Students
                 orderby classes.GradeNum, classes.ClassNum
                 select new
                 {
                     classes.GradeNum,
                     classes.ClassNum,
                     stu.Name
                 };

     var students = context.CGrade.Where(s => s.GradeNum == 1).SelectMany(
                 s => s.Students.Select(
                     st => new
                     {
                         s.GradeNum,
                         s.ClassNum,
                         st.Name
                     })).OrderBy(s => s.GradeNum).ThenBy(s => s.ClassNum);

 

  3、Where 与数据筛选

    数据筛选过滤运算是针对集合中的对象元素,根据指定的条件返回符合此条件值的数据记录。

    与数据的筛选运算有关的方法有两个,它们分别是 where 与 OfType。

    我们先来看看 where,下面示例过滤出 15 到 17 岁的童鞋:

     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;
 
     namespace LINQDemo
     {
         class Where
         {
             public void GetWhereData()
             {
                 using (var context = new StudentInfoModel())
                 {
                     Console.WriteLine("LINQ 语句:");
                     var students = from stu in context.Student
                                    where stu.Age >= 15 && stu.Age <= 17
                                    select stu;
  
                     foreach (var stu in students)
                     {
                         Console.WriteLine($"{stu.Name}:\t{stu.Age}");
                     }
 
                     Console.WriteLine("\nWhere 方法:");
                     students = context.Student.Where(s => s.Age >= 15 && s.Age <= 17);
                     foreach (var stu in students)
                     {
                         Console.WriteLine($"{stu.Name}:\t{stu.Age}");
                     }
                 }
             }
         }
     }

    如你所见,LINQ 已完美整合进 C# 语法中,我们可以再套用标准的 C# 语法或其他的 C# 运算符,进而建立更为复杂的筛选表达式。

     using System;
     using System.Collections.Generic;
     using System.Linq;
 
     namespace LINQDemo
     {
         class Where
         {
             public void DoWhere()
             {
                 int[] numberQuery = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 };
            
                 string source = "列表数据:";
                 foreach (var num in numberQuery)
                 {
                     source += num + ",";
                 }
                 Console.WriteLine(source + "\n");
 
                 IEnumerable<int> enumNumbers = from number in numberQuery
                                                where number <= 20 && number >= 10
                                                select number;
                 string result = "查询10-20之间的数字:";
                 foreach (var num in enumNumbers)
                 {
                     result += num + ",";
                 }
                 Console.WriteLine(result + "\n");
 
                 enumNumbers = from number in numberQuery
                               where IsEven(number)
                               select number;
                 result = "查询偶数:";
                 foreach (var num in enumNumbers)
                 {
                     result += num + ",";
                 }
                 Console.WriteLine(result);
             }
 
             public bool IsEven(int value)
             {
                 bool val = (value % 2) == 0;
                 return val;
             }
         }
     }

    可以看到 where 条件中调用了 IsEven 方法,来判断传值是否为偶数并返回结果,程序会根据此结果决定筛选的数据。

    但是,当 LINQ 计算的数据记录是直接连接到底层数据进行存取时,由于牵涉 SQL 语句的转换问题,所以在  where

    子句中引用独立函数时会出现无法解析的问题,因此必须将数据完全载入之后才能进一步运算。

     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;

     namespace LINQDemo
     {
         class Where
         {
             public void DoWhere()
             {
                 using(var context = new StudentInfoModel())
                 {
                     IEnumerable<Student> students = from stu in context.Student
                                                     where IsAgeRange(stu.Age)
                                                     select stu;
                     Console.WriteLine("查询 16-18 岁的学生:");
                     foreach (var stu in students)
                     {
                         Console.WriteLine($"{stu.Name} \t{stu.Age}");
                     }
                 }
             }

             private bool IsAgeRange(int? age)
             {
                 bool b = age >= 16 && age <= 18;
                 return b;
             }
         }
     }

    如同上面的示例,因为 IsAgeRange 方法无法被解析,所以就会出现如下错误:

    

    我们需要将 LINQ 语句进行 ToList 转换,以确保程序的正常运行:

     IEnumerable<Student> students = from stu in context.Student.ToList()
                                     where IsAgeRange(stu.Age)
                                     select stu;

    这么一来,由于数据完成了转换,所以程序得以正常运行:

    

 

  4、OfType 方法

    如果想要通过类型来筛选集合对象,可以通过引用 IEnumerable 的扩充方法 OfType 来实现。

    对未导入泛型技术的旧型项目进行维护时,通过 OfType 可以很方便地对特定类型进行筛选。

      实践出真知,上例子:

     using System;
     using System.Collections;
     using System.Collections.Generic;
     using System.Linq;

     namespace LINQDemo.Demo
     {
         class OfType
         {
             public void OfTypeDemo()
             {
                 ArrayList arrayList = new ArrayList()
                 {
                     "Mon","Tue","Wed","Thu","Fri","Sat","Sun",
                     "1", "2", "3", "4", "5", "6", "7",
                     1,2,3,4,5,6,7,
                 };
 
                 Console.WriteLine("ArrayList 中的数据:");
                 foreach (var al in arrayList)
                 {
                     Console.WriteLine(al.ToString() + ",");
                 }
 
                 IEnumerable<string> arrList = arrayList.OfType<string>();
          IEnumerable<string> strList = from strl in arrList select strl;
                 Console.WriteLine("\n\nOfType<string> 的筛选结果:");
                 foreach (var al in strList)
                 {
                     Console.WriteLine(al.ToString() + ",");
                 }
             }
         }
     }

    OfType 方法可以将传统的集合转换成支持 LINQ 查询的 IEnumerable 对象,它会只筛选符合类型的对象。

    这个方法相当实用, 现在结合实体数据模型进行

     using System;
     using System.Collections;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;
 
     namespace LINQDemo.Demo
     {
         class OfType
         {
             public void DoOfType()
             {
                 using (var context = new StudentInfoModel())
                 {
                     var grands = from cg in context.CGrade.ToList()
                                  select GetStudent(cg);
                     IEnumerable<rStudent> rStudents = grands.OfType<rStudent>();
                     foreach (var stus in rStudents)
                     {
                         Console.WriteLine($"{stus.Name} 的全部学生:");
                         foreach (var st in stus.Stus)
                         {
                             Console.WriteLine($"\t{st.Name}");
                         }
                     }
                 }
             }

             private object GetStudent(ClassGrade cg)
             {
                 if (cg.GradeNum == 1)
                 {
                     rStudent rStudent = new rStudent()
                     {
                         Id = cg.CGId,
                         Name = $"{cg.GradeNum}年级{cg.ClassNum}班",
                         Stus = cg.Students
                     };
                     return rStudent;
                 }else
                 {
                     return cg;
                 }
             }

             public class rStudent
             {
                 public int Id { get; set; }
                 public string Name { get; set; }
                 public IEnumerable<Student> Stus { get; set; }
             }
         }
     }

    上面的例子通过 OfType 方法过滤出了属于一年级每个班级的童鞋:

    

 

  5、排序

    排序是查询操作中非常普遍的行为,LINQ 根据指定的对象属性对集合元素进行排序。

    LINQ 中的排序方法:

    OrderBy (升序排序)、OrderByDescending(降序排序)、ThenBy(二次升序排序)、ThenByDescending(二次降序排序)以及

    Reverse(反转集合中的对象排列顺序)

    屏上得来终觉浅,觉知此事得执行:

     using System;
     using System.Linq;

     namespace LINQDemo.Demo
   {
         class OrderBy
         {
             public void GetDataOrderBy()
             {
                 using (var context = new StudentInfoModel())
                 {
                     // LINQ 语句
                     var stus = from stu in context.Student
                                orderby stu.Age, stu.Name
                                select stu;

                     //OrderBy、ThenBy方法
                     var stu_met = context.Student.OrderBy(s => s.Age).ThenBy(s => s.Name);
                     foreach (var stu in stus)
                     {
                         Console.WriteLine($"姓名:{stu.Name} \t年龄:{stu.Age}");
                     }
                 }
             }
         }
     }

    看下执行结果,如果没有出错的话,大概是按照年龄升序进行排序的:

    

    降序排序只需在字段后面加上 descending 即可:

      // LINQ 语句
   var stus = from stu in context.Student orderby stu.Age descending select stu;
   //OrderByDescending 方法
var stu_met = context.Student.OrderByDescending(s => s.Age);
     // LINQ 语句
     var stus = from stu in context.Student
                orderby stu.Age descending, stu.Name descending
                select stu;
     //OrderBy、ThenBy方法
     var stu_met = context.Student.OrderByDescending(s => s.Age).ThenByDescending(s => s.Name);

      多字段进行排序的话,需用 “,” (逗号) 进行分隔(先以年龄排序,再通过名称排序):

     // LINQ 语句
     var stus = from stu in context.Student
                orderby stu.Age, stu.Name
                select stu;
     //OrderBy、ThenBy方法
     var stu_met = context.Student.OrderBy(s => s.Age).ThenBy(s => s.Name);

    

    排序的痕迹有没有很明显?

    反转输出

    Reverse 是排序的方法之一,不过它并没有对应的 C# 语句,必须直接进行引用。

    Reverse 方法不需要任何参数。

    心动么?行动吧,再不行动,女神都老啦:

     using System;
     using System.Linq;
 
     namespace LINQDemo.Demo
     {
         class Reverse
         {
             public void DoReverse()
             {
                 using(var context = new StudentInfoModel())
                 {
                     var stus = context.Student.OrderBy(s => s.Age).ToList();
                     stus.Reverse();
                     foreach (var stu in stus)
                     {
                         Console.WriteLine($"姓名:{stu.Name} \t年龄:{stu.Age}");
                     }
                 }
             }
         }
     }

    

  6、分组

    分组通过一个指定的 key 返回分组化的数据内容,相关的语句有两种:group-by 和 group-by-into。

    ① group-by

    group 表示将要查询的数据源进行分组,其后必须连接所要搜索的集合对象,by 则是指定作为分组依据的条件值。

    分组运算返回的是一种 IGrouping<TKey, TElement> 类对象,表示一系列具有共同键值的对象集合,集合元素则

    通过此键值辨识元素所属分组,因此我们可以通过分组运算来自定义逻辑,以达到将集合中的对象进行分类的目的。

    干就完了:

     using System;
     using System.Collections.Generic;
     using System.Linq;

     namespace LINQDemo.Demo
     {
         class GroupDemo
         {
             public void DoGroup()
             {
                 List<int> monthList = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
                 // LINQ 
                 IEnumerable<IGrouping<int, int>> query = from month in monthList
                                                          group month by checkMonth(month);
                 // GroupBy 方法
                 IEnumerable<IGrouping<int, int>> query2 = monthList.GroupBy(month => checkMonth(month));
                 foreach (var group in query)
                 {
                     Console.WriteLine(group.Key > 30 ? "每月31天" : group.Key < 30 ? "二月" : "每月30天");
                     foreach (var i in group)
                     {
                         Console.WriteLine($"{i}-{group.Key.ToString()}");
                     }
                     Console.WriteLine("\n");
                 }
             }
 
             public int checkMonth(int month)
             {
                 int resultDays;
                 resultDays = DateTime.DaysInMonth(2020, month);
                 return resultDays;
             }
         }
     }

 

    上面示例给每个月按照天数进行了分组归类:

    

    ② group-by-into

    into 表示指定一个暂存的标识变量,用来存储每一个分组后的结果,然后针对每一个分组结果进一步执行其他的查询运算。

    into 最后会将分组的结果存储于指定的标识变量中,因此一旦启用 into 就必须进一步在其后编写处理表达式,然后以 select

    或另一个 group 作为结束。

    把上例中的 LINQ 语句进行调整下:

     // LINQ 
     IEnumerable<IGrouping<int, int>> query = from month in monthList
                                              group month by checkMonth(month) into o
                                              orderby o.Key
                                              select o;  //into 把分组结果的引用存储给了指定变量 o,然后通过 Key 对其进行了排序。
     // GroupBy 方法  用 Select 实现 into 的功能
     IEnumerable<IGrouping<int, int>> query2 = monthList.GroupBy(month => checkMonth(month)).Select(g => g).OrderBy(g => g.Key);

     

    可以看到月份分组按照天数进行排序了。

    接下来,结合 ADO.NET 实体数据模型来探讨下数据库的分组操作。

    直接看示例吧:

     using System;
     using System.Collections.Generic;
     using System.Linq;

     namespace LINQDemo.Demo
     {
         class GroupDemo
         {
             public void DoADOGroup()
             {
                 using (var context = new StudentInfoModel())
                 {
                     IEnumerable<IGrouping<int?, Student>> groups = from stu in context.Student
                                                                    group stu by stu.Age;
                     foreach (var gp in groups)
                     {
                         Console.WriteLine(gp.Key + "岁的童鞋:");
                         foreach (var stu in gp)
                         {
                             Console.WriteLine($"姓名:{stu.Name} \t{stu.Age}");
                         }
                         Console.WriteLine("\n");
                     }
                 }
             }
         }
     }

    还是看结果吧:

    

    我们还可以将分组的规则进行进一步的封装,以便简化 LINQ 语句:

     using System;
     using System.Collections.Generic;
     using System.Linq;
 
     namespace LINQDemo.Demo
     {
         class GroupDemo
         {
             public void DoGroupToMethod()
             {
                 using (var context = new StudentInfoModel())
                 {
                     IEnumerable<IGrouping<string, Student>> gps = from stu in context.Student.ToList()
                                                                   group stu by CheckAge(stu.Age ?? 0);
   
                     foreach (var gp in gps)
                     {
                         Console.WriteLine(gp.Key);
                         foreach (var stu in gp)
                         {
                             Console.WriteLine($"姓名:{stu.Name} \t年龄:{stu.Age}");
                         }
                         Console.WriteLine("\n");
                     }
                 }
             }
 
             string CheckAge(int age)
             {
                 if (age >= 18) return "成年";
                 else if (age >= 16 && age <= 18) return "青年";
                 else return "少年";
             }
         }
     }

     

    通过这种方式我们可以写更复杂的分组处理逻辑。

    group-by-into 同样也可以结合 ADO.NET 实体数据模型来处理数据。

     using System;
     using System.Collections.Generic;
     using System.Linq;
 
     namespace LINQDemo.Demo
     {
         class GroupDemo
         {
             public void GroupInto()
             {
                 using (var context = new StudentInfoModel())
                 {
                     IEnumerable<IGrouping<int, ClassGrade>> gps = from cg in context.CGrade.ToList()
                                                                   group cg by cg.GradeNum into cgp
                                                                   orderby cgp.Key 
                                                                   select cgp;
                     foreach (var gp in gps)
                     {
                         Console.WriteLine($"{gp.Key}年级:");
                         foreach (var cg in gp)
                         {
                             foreach (var stu in cg.Students)
                             {
                                 Console.WriteLine($"姓名:{stu.Name}  \t班级:{cg.ClassNum}班");
                             }
                          }
                          Console.WriteLine("\n");
                     }
                 }
             }
         }
     }

    

     OK,篇幅过长…来日方长!拜了个拜!

 

 

posted @ 2020-12-12 17:19  LI小白  阅读(245)  评论(0编辑  收藏  举报