EFCore:详谈Entity Framework Code高级查询
1、相关数据的加载
前面我们已经谈过导航属性,这里我们详细谈下导航属性的几种加载方法
<1>预先加载表示从数据库中加载关联数据,最初查询的时候就加载。
<2>显式加载 初始查询没有加载,需要的时候稍后从数据库中显式加载关联数据。
<3>延迟加载 表示在访问导航属性时,从数据库中以透明方式加载关联数据。
1.1 预先加载
预先加载有两种使用方式
<1>在查询的时候使用Include;
dbContext.Students.Include(x=>x.Courses).ToArray();
<2>在DbContext的OnModelCreating中使用Navigation和AutoInclude属性查询的时候预先加载;
modelBuilder.Entity<Student>(entity => { entity.Property(x => x.StudentId).ValueGeneratedOnAdd();//设置Id自增 entity.Property(x => x.Name).HasMaxLength(50).IsUnicode().IsRequired();//设置姓名最大长度为50,字符为unicode,不能为空 entity.Property(x => x.Sex).HasMaxLength(5).IsUnicode().IsRequired();//设置性别最大长度为5 字符为Unicode,不能为空 entity.HasOne(x => x.Address).WithOne(x => x.Student).HasForeignKey<StudentAddress>(ad => ad.StudentId);//一对一只需要配置一个类就行了, entity.Navigation(x => x.Courses).AutoInclude(); entity.Navigation(x => x.Address).AutoInclude(); });
两种方式不要同时使用,在DbContext里进行配置后,每次查询都会自动加载。
经过配置后,查询的时候,会显示如下,这时候我们会发现 Student 和StudentAdress出现了循环引用,循环引用在Json序列化,或者其他的框架中可能会出错。
在某些序列化框架不允许使用循环引用。 例如,Json.NET 在发现循环引用的情况下,会引发以下异常。Newtonsoft.Json.JsonSerializationException。
如果正在使用 ASP.NET Core,则可以将 Json.NET 配置为忽略在对象图中找到的循环引用。 此配置是通过
Startup.cs 中的 ConfigureServices(…) 方法完成的。
services.AddMvc() .AddJsonOptions( options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore );
另一种方法是使用 [JsonIgnore] 特性修饰其中一个导航属性,该特性指示 Json.NET 在序列化时不遍历该导航属性。
1.2 显示加载
将配置中自动添加导航属性配置注释掉,main函数里写下如下代码:
var student = dbContext.Students .Single(x=>x.StudentId== 2022001); Console.WriteLine($"============before====Student Address:{student.Address?.Address??"empty"},Courese:{student.Courses?.Count??0}===="); dbContext.Entry(student) .Collection(s=>s.Courses) .Load(); dbContext.Entry(student) .Reference(s=>s.Address) .Load(); Console.WriteLine($"===========After=====Student Address:{student.Address.Address},Courese:{student.Courses.Count}");
我们在首次查询的时候,并没有包含相关的导航属性,而是在查询后,通过对实例对象的采用collection或者Reference来加载相关的导航属性,这样也能进行相关的导航属性加载。
除此之外,如果我们只要每个学生所选的课程,只需要:
var student = dbContext.Students .Single(x => x.StudentId == 2022001); var coureseCount = dbContext.Entry(student) .Collection(s => s.Courses) .Query().Count(); dbContext.Entry(student) .Reference(s => s.Address) .Load(); Console.WriteLine($"===========After=====Student Address:{student.Address.Address},Courese:{student.Courses.Count},CourseCount:{coureseCount}");
Course的导航属性并没有加载到内存中,但是我们也知道了这个学生所学的课程数目。
查看下面代码,这个是个复查的查询,一开始你可能不明白这个查询结果返回的是什么,
但是我们一点点拆解,Collection之后返回的是这个学生所学的课程,where之后是对课程的筛选,这个筛选条件是授课老师是王老师,所以这个查询返回的结果是这个学生所选的课程中王老师所教的课程。 试想下我们如果用Sql语句来查询这个会有多复杂,而且我们在此次查询中并没有采用 include或者Reference等将课程的导航属性 Teacher加载进来,但是EFCore也给了我们正确的查询结果。
var datas = dbContext.Entry(student) .Collection(s => s.Courses) .Query().Where(s=>s.Teacher.Name.Contains("王")).ToList();
1.3 延迟加载
众所周知在EF 6 及以前的版本中,是支持懒加载(Lazy Loading)的,可惜在EF Core 并不支持,必须使用Include方法来支持导航属性的数据加载。不过现在EF Core的开发团队打算恢复对这一功能的支持。
懒加载也可以叫做按需加载、延迟加载。可以分两方面来理解,一方面指暂时不需要该数据,不用在当前马上加载,而可以推迟到使用它时再加载;另一方面指不确定是否将会需要该数据,所以暂时请不要加载,待确定需要后再加载它。懒加载是一种很重要的数据访问特性,可以有效地减少与数据源的交互(注意,这里所提的交互不是指交互次数,而是指交互的数据量),从而提升程序性能。
在EFCore 中使用延迟加载,需要两步走:
<1>先安装Microsoft.EntityFrameworkCore.Proxie这个Nuget包
<2>在DbContext类中的OnConfiguring启用,添加 optionsBuilder.UseLazyLoadingProxies();
将所有的导航属性改成Virtual ,我们以Student为例:
public virtual StudentAddress Address { get; set; } public virtual IList<Course> Courses { get; set; } = new List<Course>();
可能到这里你还不理解懒加载有什么用,下面我们写一段代码。
var students = dbContext.Students.Where(x => x.Age > 18).ToList(); foreach (var student in students) { Console.WriteLine($"{student.Name}-{student.Address.Address}"); }
可以看到,我们并没有使用Include,也没有使用自动配置导航属性,但是我们却可以访问到Student的Address。而从监视中可以看到,Address属性,在第一次查询返回的时候,只是一个代理形式的类,并不是一个Address实例类,这就是懒加载的特点,按需分配,需要的时候自动帮你处理,不需要的不加载。
2、数据库函数
EFCore提供了一系列的内置函数供我们使用,例如我们前面使用的字符串函数Contains、StartWith、EndsWith、string.IsNullOrEmpty等,除此之外,我们也可以自定义函数,在EFCore中使用。内置函数还有一系列以EF.Functions开头的,比如EF.Functions.Like。
除此之外,我们也可以自定义函数、使用在Navicat里自定一个函数,来进行统计某个老师的所教授课程的数目。
create function CountCourse(id INT) returns int begin declare c int; SELECT COUNT(*) FROM Courses WHERE TeacherId=id INTO c; return c; end;
在DbContext里修改配置,首先定义一个函数,主要不需要函数体实现部分
public int ComputeTeacherCourse(int teacherId) => throw new NotSupportedException();
然后在OnModelCreating里配置如下代码
modelBuilder.HasDbFunction(typeof(EFLearnDbContext).GetMethod(nameof(ComputeTeacherCourse), new[] { typeof(int) })) .HasName("CountCourse");
编写一个筛选授课数目大于1的老师
var teachers = dbContext.Teachers.Where(x=>dbContext.ComputeTeacherCourse(x.TeacherId)>1).ToArray(); foreach (var teacher in teachers) { Console.WriteLine($"{teacher.Name}"); }
3、分页查询
在查询时,如果结果集数据量很大,比如几万行数据,不如分页显示,一页多少条,分页查询的时候最好采用一种确定的排序方式,通用的分页查询如下,其中page是第几页,从1开始,Size是每一页的数据大小。
dbContext.Students.OrderBy(x => x.StudentId).Skip((page-1)*size).Take(size);
4、全局查询筛选器
全局查询筛选器是应用于元数据模型(通常为 OnModelCreating)中的实体类型的 LINQ 查询谓词。 查询谓词即通常传递给 LINQ Where 查询运算符的布尔表达式。 EF Core 会自动将此类筛选器应用于涉及这些实体类型的任何 LINQ 查询。 EF Core 还将其应用于使用 Include 或导航属性进行间接引用的实体类型。 此功能的一些常见应用如下:
软删除 - 实体类型定义 IsDeleted 属性。
多租户 - 实体类型定义 TenantId 属性。
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId); modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
全局筛选还可以作用在导航属性上,如果需要禁用导航属性,可使用 IgnoreQueryFilters 运算符对各个 LINQ 查询禁用筛选器。
blogs = db.Blogs .Include(b => b.Posts) .IgnoreQueryFilters() .ToList();