大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ADO&EF 修改讲义

Find()只能通过主键查询,更复杂的查询需要通过Linq才能完成。

演示

查出姓王的学生:

Student students = context.Students
    .Where(s => s.Name.StartsWith("王"))
    .ToList();

演示:查看log中的SQL语句,

WHERE [s].[Name] IS NOT NULL AND ([s].[Name] LIKE N'王%')

#体会#:IS NOT NULL的严谨!

也可以使用Linq表达式:

from s in context.Students
where s.Name.StartsWith("王")

Linq中其他方法也可以使用,比如排序:

Student student = context.Students
    .Where(s => s.Name.StartsWith("王"))
    .OrderByDescending(s=>s.Age)
    .FirstOrDefault();

最终的结果可以是集合,也可以是单个。


背后原理

但这里的Where()方法和我们之前的不一样:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source, Func<TSource, bool> predicate);

IQueryable和Expression

首先,DbSet<T>实现了IQueryable接口;

所以,它的Where()方法定义的参数类型是Expression

返回值还是IQueryable:

public static IQueryable<TSource> Where<TSource>(
    this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

#常见面试题:IQueryable/IEnumerable/IList的区别?#

在Linq to EF中,IQueryable和IEnumerable的区别,就类似于Linq to Object中IEnumerable和IList的区别。

断点log演示:

  1. IQueryable并不真正存储查询结果,而是存储“动态查询语句”(dynamic query)
  2. 直到真正需要(被foreach、ToList()、Single()、Count()等
  3. 这些语句会被C#编译成表达式树(Expression Tree)
  4. 然后被EF的Provider翻译成SQL语句
  5. 并连接数据库执行
  6. 利用查询结果生成entity(如果当前DbContext中还没有该entity的话)
IQueryable<Student> query = context.Students
    .Where(s => s.Name.StartsWith("王"));
query = query.OrderByDescending(s => s.Score);
Student student = query.FirstOrDefault();

Set()

有时候DbSet<T>没有被声明为DbContext的属性(是在OnModelCreating()中声明的),但我们又需要使用它(开始Linq查询),怎么办?

可以调用DbContext的Set<T>()方法:

DbSet<Student> students = context.Set<Student>();


Provider的作用

不同的数据库需要不同的provider,“即插即用”

演示:改动由DbContext里定义的Database Provider,生成增删改查的SQL语句并执行

  1. nuget得到Pomelo.EntityFrameworkCore.MySql
  2. optionBuilder中设置连接字符串和版本号:
    optionsBuilder
        .UseMySql("server=localhost;Uid=root;database=17bang;Charset=utf8",
        ServerVersion.Parse("'8.0.25'"))

mysql版本号获取方法:

select version();

Translate不成

并不是所有的Linq语句都能够通过provider正确的生成。

比如,查出每个老师教了多少门课程。

这样的Linq在to Object的时候是没有问题的:

var result = context.Majors  
    .GroupBy(m => m.Teacher)
    .ToDictionary(g => g.Key, g => g.Count())
    ;

但是to EF的时候,就会报错:

The LINQ expression 'DbSet<Major>()……elementSelector: m => m.Outer)' could not be translated. 

就是没办法将上述Linq表达式翻译/转换(translate)成SQL的意思。并且给出了两种解决方案:

  1. 用另外一种形式重写Linq查询语句,比如:
    var result = context.Majors
        .GroupBy(m => m.TeacherId /* m.Teacher无法translate */)
        //只有Select()才会被translate,不能省略
        .Select(g => new { g.Key, count = g.Count() })
        .ToDictionary(g => g.Key, g => g.count)
        ;
  2. 通过插入AsEnumerable()/ToList()等将数据“整体”的从数据库取出来之后再group()
    var result = context.Majors
        .Include(m => m.Teacher).AsEnumerable()
        //或者:.ToList()

#理解:AsEnumerable()和ToList()的区别#

他们都能促成数据库查询,但是:

  • ToList()能将查询结果集存放到一个List集合中,然后关闭数据库连接
  • AsEnumerable()没有List集合,查询结果仍由DataReader管理,所以数据库连接不能关闭

演示:没有Include()的AsEnumerable()在lazyload Teacher时抛异常:

There is already an open DataReader associated with this Connection……

常见坑

在3.0以前,如果无法translate成SQL语句,EF会自动调用AsEnumerable(),要当心由此带来的性能问题。

在7.0以后,GroupBy()可以直接作为最终语句,但是会生成order by语句,(*/ω\*),坑!建议:始终在group之后Select。

允许Sum()等聚合函数返回null:

.Sum(m =>(double?)m.Score)    //可空类型强转转换


自定义SQL

当SQL语句无法由Linq语句自动生成(translate),或者

生成的SQL语句无法满足要求(一般是性能)的时候,

我们可以自己写出SQL语句,然后交由EF执行:

增删改

经由context.Database调用:
  1. ExecuteSqlRaw(),或者
  2. ExecuteSqlInterpolated()

Raw vs Interpolated

如果要避免SQL注入,ExecuteSqlRaw()需要使用@age并传入SqlParameter等(使用参数化查询)。

ExecuteSqlInterpolated()会自动完成这些工作,超赞!(log演示)

int rows = context.Database.ExecuteSqlInterpolated(
    $"UPDATE Students SET Score = {score} WHERE Score = 0;");
Console.WriteLine(rows);

ExecuteSqlX()只能返回受影响的行数。所以

查询

仍从DbSet开始,调用:

  • FromSqlRaw(),或者
  • FromSqlInterpolated(),
注意SQL语句的查询结果集一定要和DbSet泛型参数相“匹配”,这样才能能保证IQueryable<T>的生成。

根据两方法返回的IQueryable还可以进一步的被Linq过滤:

var result = context.Students.FromSqlInterpolated(
    $"SELECT * FROM Students WHERE Score > {score}")
    .OrderBy(s => s.Score);
log演示:EF使用表表达式生成OrderBy()的SQL语句


禁用追踪

如果只是单条语句禁用,可以在DbSet上调用AsNoTracking():

IList<Student> students = context.Students
    .AsNoTracking()
    .Where(x => x.Score > 80)
    .ToList();

如果要对DbContext中所有操作都不予追踪:

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

在查询之后添加两行代码:

//id=3的studentScore>80,id=1的<80
Student s3 = context.Students.Find(3);
Student s1 = context.Students.Find(1);

对比演示:

  • log中有无:Context 'SqlDbContext' started tracking 'Student' entity with key '{Id: 1}'.
  • 后续获取s3和s1是否会触发SQL查询

PS:EF目前还没有提供官方的/内置的二级缓存方案。


作业

ORM概述第9/10/11/12题。

学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ADO&EF 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码