02020503 EF Core高级03-分页查询、IQuerable底层的实现形式、DataReader、DataTable、EF Core中的异步方法
1. EF Core分页查询(视频3-27)
1.1 分页查询的实现
1、Skip(3).Take(8) 最好显式指定排序规则,Skip表示跳过多少条数据,Take表示取几条数据。
2、需要知道满足条件的数据的总条数:用IQueryable的复用
LongCount和Count
3、页数:long pageCount = (long)Math.Ceiling(count * 1.0 / pageSize);
1.2 查看int和long类型的数据范围
using System;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){Console.WriteLine(int.MaxValue);Console.WriteLine(long.MaxValue);}}
}控制台输出:
2147483647
9223372036854775807
1.3 分页查询示例
- 在02020409章5.2节基础上继续
// 数据库中Articles表添加如下数据
Id Title Message Price
1 杨中科入选中科院 大新闻 0
2 微软发布.NET 10.0 中新闻 0
3 微软发射微星 小新闻 0
4 中国发射小行星探测器 劲爆新闻 0
5 111 111 0
8 222 222 0
9 333 333 0
10 444 444 0
11 555 555 555
12 666 666 0
13 777 777 0
14 888 888 0
15 999 999 0
16 1010 1010 0
17 1111 1111 0
18 12121 1212 0
19 1313 1313 0
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Program.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;namespace OneToMany
{class Program{static void Main(string[] args){PrintPage(1, 3); // 取第一页,每页3条。Console.WriteLine("********************");PrintPage(3, 3); // 取第三页,每页3条。Console.ReadLine();}/// <summary>/// 打印某一页数据,同时打印总页数/// </summary>/// <param name="pageIndex">页码(从1开始)</param>/// <param name="pageSize">每一页展示的条数</param>static void PrintPage(int pageIndex, int pageSize){using (MyDbContext ctx = new MyDbContext()){IQueryable<Article> arts = ctx.Articles.Where(a => !a.Title.Contains("杨中科")); // 取出所有Title中不含有杨中科的数据var items = arts.Skip((pageIndex - 1) * pageSize).Take(pageSize);foreach (var item in items){Console.WriteLine(item.Title);}long Count = arts.LongCount(); // Celling表示取天花板,1.1取2。乘以1.0为了转为小数运算。long pageCount = (long)Math.Ceiling(Count * 1.0 / pageSize);Console.WriteLine("总页数" + pageCount);}}}
}控制台输出:
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
总页数6
********************
444
555
666
总页数6// SQL语句
SELECT COUNT_BIG(*)FROM [T_Articles] AS [t]WHERE NOT ([t].[Title] LIKE N'%杨中科%')
2. IQuerable底层如何读取数据(视频3-28)
2.1 对比DataReader和DateTable
1、DataReader:分批从数据库服务器读取数据。内存占用小、 DB连接占用时间长;
2、DataTable:把所有数据都一次性从数据库服务器都加载到客户端内存中。内存占用大,节省DB连接。
2.2 验证IQuerable用什么方式
1、用insert into select多插入一些数据,然后加上Delay/Sleep的遍历IQueryable。在遍历执行的过程中,停止SQLServer服务器。
IQueryable内部就是在调用DataReader。
2、优点:节省客户端内存。
缺点:如果处理的慢,会长时间占用连接。
3. 验证IQuerable以DataReader形式读取数据示例
- 在本课1.3节上继续
3.1 在数据库插入数据
SSMS → CoreDataDB → 右键 → 新建查询insert into T_Articles(Title,Message) // 插入数据
select Title,Message from T_Articles // 查询数据说明:将数据库中查询的数据插入到数据库中,这样数据库可以以指数形式增加。
3.2 代码示例
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles){Console.WriteLine(item.Title);Thread.Sleep(100); // 没打印一次暂停100ms}}Console.ReadLine();}}
}控制台输出:
杨中科入选中科院
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
111
222
333
444
555
666
777
... // 不断打印
3.3 关闭数据库
任务管理器 → 进程 → SQL Server VSS Writer -64 Bit → 右键 → 打开服务 → SQL Server(MSSQLSERVER) → 停止

3.4 程序抛出异常
Microsoft.Data.SqlClient.SqlException:“A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)”异常信息:数据库物理连接异常。
3.5 总结
- 采用的是DataReader形式读取。分批从数据库服务器读取数据。内存占用小、 DB连接占用时间长;
- 如果有很多读取,那么会长时间占用数据库连接。
4. 验证IQuerable以DataTable形式读取数据示例
4.1 一次性加载所有数据
1、一次性加载数据到内存:用IQueryable的ToArray()、ToArrayAsync()、ToList()、ToListAsync()等方法。
2、等ToArray()执行完毕,再断服务器试一下。
4.1 代码示例
- 在本课3.2节上继续
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles.ToArray()) // 或者为foreach (var item in ctx.Articles.ToList()){Console.WriteLine(item.Title);Thread.Sleep(100); // 没打印一次暂停100ms}}Console.ReadLine();}}
}控制台输出:
杨中科入选中科院
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
111
222
333
444
555
666
777
... // 不断打印说明:此时程序会卡一下,然后开始输出。因为卡顿的时间就是在读取数据库中的所有数据。
4.2 断开数据库连接
- 重复3.3中操作,此时程序不会抛出异常。
5. 何时需要一次性加载
5.1 一次性加载的适用性
1、场景1:遍历IQueryable并且进行数据处理的过程很耗时。
2、场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回IQueryable的。必须一次性加载返回。
3、场景3:多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。把连接字符串中的MultipleActiveResultSets=true删掉,其他数据库不支持这个。
5.2 场景2分析
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var items = QuerryZYK(); // @2 ctx连接用完后销毁foreach (var item in items) // @3{Console.WriteLine(item.Title);}}Console.ReadLine();}static IQueryable<Article> QuerryZYK(){using (MyDbContext ctx = new MyDbContext()){return ctx.Articles.Where(a => !a.Title.Contains("杨中科")); // @1}}}
}抛出异常:System.ObjectDisposedException:“Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
ObjectDisposed_ObjectName_Name”说明:
1. 在@1处并没有查询数据库,而是在@3处使用foreach时才开始执行。而程序在@2处已经销毁了。
2. 此处即使将static IQueryable<Article> QuerryZYK()定义为static IEnumerable<Article> QuerryZYK()也不行。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var items = QuerryZYK();foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}static IQueryable<Article> QuerryZYK(){MyDbContext ctx = new MyDbContext(); // 只调用,不销毁ctx。return ctx.Articles.Where(a => !a.Title.Contains("杨中科"));}}
}控制台输出:
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
111
...
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
...说明:此时控制台虽然能正常打印,但是不合理。因为没有销毁ctx,每调用一次就会创建ctx对象,这样会有内存泄漏的危险。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 使用一次性加载来实现
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var items = QuerryZYK();foreach (var item in items){Console.WriteLine(item.Title);}}Console.ReadLine();}static IEnumerable<Article> QuerryZYK() // 返回IEnumerable{using (MyDbContext ctx = new MyDbContext()){return ctx.Articles.Where(a => !a.Title.Contains("杨中科")).ToArray(); // 一次性加载}}}
}控制台输出:
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
111
...
微软发布.NET 10.0
微软发射微星
中国发射小行星探测器
...说明:
1. 一次性都加载到内存中之后,再销毁ctx也不会出问题,后面的数据都是从内存中加载。
2. 通常来说,不建议方法的返回值为IQuerable,而是返回IEnumerable。
5.3 场景3分析
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;namespace OneToMany
{class MyDbContext : DbContext{public DbSet<Article> Articles { get; set; }public DbSet<Comment> Comments { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True"; // 去掉;MultipleActiveResultSets=trueoptionsBuilder.UseSqlServer(connStr);// optionsBuilder.LogTo(Console.WriteLine);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item01 in ctx.Articles) // 第一个遍历{Console.WriteLine(item01.Title);foreach (var item02 in ctx.Comments) // 第二个遍历{Console.WriteLine(item02.Message);}}Console.ReadLine();}}}
}抛出异常:System.InvalidOperationException:“There is already an open DataReader associated with this Connection which must be closed first.”说明:在ADO.NET中默认不支持多个DataReader连接的。同时遍历2个IQuerable就是遍历两个DataReader,是不支持的。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 在SQL Server中,只需要加上MultipleActiveResultSets=true就行。即允许多个活动的结果集打开(允许多个DataReader运行)。
using Microsoft.EntityFrameworkCore;
using System;namespace OneToMany
{class MyDbContext : DbContext{public DbSet<Article> Articles { get; set; }public DbSet<Comment> Comments { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";optionsBuilder.UseSqlServer(connStr);// optionsBuilder.LogTo(Console.WriteLine);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item01 in ctx.Articles) // 第一个遍历{Console.WriteLine(item01.Title);foreach (var item02 in ctx.Comments) // 第二个遍历{Console.WriteLine(item02.Message);}}Console.ReadLine();}}}
}控制台输出:
...
太牛了
微软不过如此
微软真牛
微软真水
999
太牛了
微软不过如此
微软真牛
微软真水
1010
...
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:在其它数据库中,MultipleActiveResultSets=true是否支持待考证(MySQL数据库是不支持的)。
5.4 不用MultipleActiveResultSets=true配置怎么解决
// 一次性加载到内存里面,在内存里面完成遍历
using Microsoft.EntityFrameworkCore;
using System;namespace OneToMany
{class MyDbContext : DbContext{public DbSet<Article> Articles { get; set; }public DbSet<Comment> Comments { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True"; // 不再配置MultipleActiveResultSets=trueoptionsBuilder.UseSqlServer(connStr);// optionsBuilder.LogTo(Console.WriteLine);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;namespace OneToMany
{class Program{static void Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item01 in ctx.Articles.ToArray()) // 一次性加载{Console.WriteLine(item01.Title);foreach (var item02 in ctx.Comments.ToArray()) // 一次性加载{Console.WriteLine(item02.Message);}}Console.ReadLine();}}}
}控制台输出:
...
太牛了
微软不过如此
微软真牛
微软真水
999
太牛了
微软不过如此
微软真牛
微软真水
1010
...说明:
1. 此时内存占用会比较高,如果只是只有几十条,几百条,最多几万条是可以全部加载到内存的。如果更多,内存占用不合理。
2. 这种形式在常规业务中是用的比较多。实际开发中依据项目需求来决定。
6. EF Core中的异步方法(视频3-29)
6.1 对应的异步方法
1、SaveChanges()、SaveChangesAsync()
2、异步方法大部分是定义在Microsoft.EntityFrameworkCore这个命名空间下EntityFrameworkQueryableExtensions等类中的扩展方法,记得using。
3、AddAsync()、AddRangeAsync()、AllAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync等
6.2 非异步方法
IQueryable的这些异步的扩展方法都是“立即执行”方法,而GroupBy、OrderBy、Join、Where等“非立即执行”方法则没有对应的异步方法。为什么?因为:“非立即执行”方法并没有实际执行SQL语句,并不是消耗IO的操作。而异步方法对于耗时操作,会产生线程阻塞。总结就是:非终结方法不消耗IO。
6.3 异步遍历IQuerable
方式1、ToListAsync()、ToArrayAsync()。结果集不要太大。
方式2、await foreach (Book b in ctx.Books.AsAsyncEnumerable())不过,一般没必要这么做。比较推荐用如下同步方法写法,如果如下同步方法出现了性能瓶颈,再考虑上述两个方式。很少出现这样的情况。
foreach(var a in ctx.Articles)
{Console.WriteLine(a.Title)
}
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※