EF Core上下文DbContext相关配置和生命周期

  • Post author:
  • Post category:其他


一. 配置相关


1. 数据库连接字符串的写法

(1).账号密码:Server=localhost;Database=EFDB01;User ID=sa;Password=123456;

(2).windows身份:Server=localhost;Database=EFDB01;Trusted_Connection=True;


2. 命令超时和默认关闭状态追踪

(1).命令超时: providerOptions => providerOptions.CommandTimeout(60),表示60秒超时。

(2).关闭查询状态追踪:UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking),表示关闭追踪;


(PS: 默认是 QueryTrackingBehavior.TrackAll 开启追踪)

详见下面案例:

a. QueryTrackingBehavior.NoTracking: 对应的是Detached状态,即有游离状态。

b. QueryTrackingBehavior.TrackAll: 对应的是Unchanged状态,即未发生改变的。

1             {
2                 var d1 = context1.T_UserInfor.First();
3                 //关闭跟踪对应的是:Detached;开启跟踪对应的是:Unchanged
4                 var state = context1.Entry(d1).State;
5             }


3. 日志记录

最好在控制台中配置,能输出生成的SQL语句。


配置代码分享:

复制代码

 1  public partial class EFDB01Context : DbContext
 2     {
 3         //public static readonly LoggerFactory MyLoggerFactory = new LoggerFactory(new[] { new ConsoleLoggerProvider((_, __) => true, true) });
 4         public static readonly LoggerFactory MyLoggerFactory  = new LoggerFactory(new[]
 5          {
 6                 new ConsoleLoggerProvider((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information, true)
 7          });
 8         public EFDB01Context()
 9         {
10 
11         }
12         public virtual DbSet<T_RoleInfor> T_RoleInfor { get; set; }
13         public virtual DbSet<T_UserInfor> T_UserInfor { get; set; }
14         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
15         {
16             optionsBuilder.UseSqlServer("Server=localhost;Database=EFDB01;User ID=sa;Password=123456;").UseLoggerFactory(MyLoggerFactory);
17         } 
18     }

复制代码


输出结果:


4. 连接弹性

详见官方文章:

连接复原-EF Core | Microsoft Docs


5. 线程安全问题

(1).说明

EF Core不支持在同一DbContext实例上运行多个并行操作。这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。因此,应await立即异步调用, 或对并行DbContext执行的操作使用单独的实例。

当EFCore检测到并行使用某个DbContext实例时, 触发InvalidOperationException异常;但是当并发访问未被检测时, 可能会导致未定义的行为、应用程序崩溃和数据损坏.

(2).情况一:在对同一 DbContext 启动任何其他操作之前, 忘记等待异步操作完成。

使用异步方法, EF Core 可以启动以非阻止方式访问数据库的操作。 但是, 如果调用方不等待其中一种方法完成, 并继续在上DbContext执行其他操作,则的DbContext状态可能会损坏。

(3).情况二:Core MVC通过依赖关系注入在多个线程之间隐式共享 DbContext 实例

默认AddDbContext情况下,注入的上下文是请求内单例的,所以每个请求都获得了单独的DbContext,请求与请求之间的DbContext是线程安全的;但是,在同一个请求中,任何并行显式执行多个线程的代码都应该DbContext确保不会同时访问实例,否则会出现上述“情况一”的状况。

(4). 案例说明:

要避免

下面案例的那种情况,公用同一个上下文,

是没法保证线程安全的


代码分享:

复制代码

 1             {
 2                 var optionsBuilder = new DbContextOptionsBuilder<EFDB01Context>();
 3                 optionsBuilder.UseSqlServer(configuration.GetConnectionString("EFStr"));
 4                 EFDB01Context context = new EFDB01Context(optionsBuilder.Options);
 5                 Task.Run(() =>
 6                 {
 7                     while (true)
 8                     {
 9                         var u1 = new T_UserInfor()
10                         {
11                             id = Guid.NewGuid().ToString("N"),
12                             userName = "ypf2",
13                             userSex = "男2",
14                             userAge = 19,
15                             addTime = DateTime.Now
16                         };
17                         context.Set<T_UserInfor>().Add(u1);
18                         int result = context.SaveChanges();
19                     }
20                 });
21 
22                 Task.Run(() =>
23                 {
24                     while (true)
25                     {
26                         var u1 = new T_UserInfor()
27                         {
28                             id = Guid.NewGuid().ToString("N"),
29                             userName = "ypf2",
30                             userSex = "男2",
31                             userAge = 19,
32                             addTime = DateTime.Now
33                         };
34                         context.Set<T_UserInfor>().Add(u1);
35                         int result = context.SaveChanges();
36                     }
37                 });
38                 Task.Delay(TimeSpan.FromMinutes(15)).Wait();
39             }

复制代码


6. 连接数和连接池问题


(1).查询数据库连接数语句

:SELECT * FROM SYSPROCESSES WHERE DBID = DB_ID(‘数据库名’)


(2).手动设置连接池的最大(小)数量

: “Server=localhost;Database=Test;Trusted_Connection=True;Max Pool Size=100;Min Pool Size=5”


(3).连接池的运行原理


概念

:连接到数据源可能需要很长时间。 为了最大程度地降低打开连接的成本,ADO.NET 使用一种称为连接池的优化技术,这会最大程度地降低重复打开和关闭连接的成本。


A.

当一个程序执行Connection.open()时候,ADO.Net就需要判断,此连接是否支持Connection Pool (Pooling 默认为True)

①:如果指定为False, ADO.Net就与数据库之间创建一个连接,然后返回给程序。

②:如果指定为 True,ADO.Net就会根据ConnectString创建一个Connection Pool,然后向Connection Pool中填充Connection。填充多少个Connection由Min Pool Size (默认为0)属性来决定。例如如果指定为5,则ADO.Net会一次与SQL数据库之间打开5个连接,然后将4个Connection,保存在 Connection Pool中,1个Connection返回给程序。


B.

当程序执行到Connection.close() 的时候。如果Pooling 为True,ADO.net 就把当前的Connection放到Connection Pool并且保持与数据库之间的连接。

同时还会判断Connection Lifetime(默认为0)属性,0代表无限大,如果Connection存在的时间超过了Connection LifeTime,ADO.net就会关闭的Connection同时断开与数据库的连接,而不是重新保存到Connection Pool中。


C.

当下一次Connection.Open() 执行的时候,ADO.Net就会判断新的ConnectionString与之前保存在Connection Pool中的Connection的connectionString是否一致。


D.

ADO.Net需要判断当前的Connection Pool中是否有可以使用的Connection(没有被其他程序所占用),如果没有的话,ADO.Net就需要判断ConnectionString设 置的Max Pool Size (默认为100)

①. 如果Connection Pool中的所有Connection没有达到Max Pool Size,ADO.net则会再次连接数据库,创建一个连接,然后将Connection返回给程序。

②. 如果已经达到了 Max Pool Size,ADO.Net就不会再次创建任何新的连接,而是等待Connection Pool中被其他程序所占用的Connection释放,这个等待时间受SqlConnection.ConnectionTimeout(默认是15 秒)限制,也就是说如果时间超过了15秒,SqlConnection就会抛出超时错误。


E.

如果有可用的Connection,从Connection Pool 取出的Connection也不是直接就返回给程序,ADO.Net还需要检查ConnectionString的ConnectionReset属性 (默认为True)是否需要对Connection 做一次reset。


更详细的介绍请参考官方文档:

SQL Server连接池 – ADO.NET | Microsoft Docs


7. 总结

(1).SaveChanges的时候数据库连接自动释放,所以不需要手动释放。

(2).调用using的方法可以,因为dispose里讲很多东西都滞空了,完全没问题;但在Core MVC中,EF上下文都是通过依赖注入,能控制生命周期,所以不再需要using。

(3).手动open然后手动close,连接数没有释放,因为连接池的概念,当然你可以手动配置连接池数目(强制删除连接池:ClearAllPools),只有当IIS关闭,连接才彻底释放。

二. 生命周期


1.源码分析

public enum ServiceLifetime { Singleton = 0, Scoped = 1, Transient = 2 }

(1).Singleton :整个应用程序生命周期以内只创建一个实例。

(2).Scoped: 在同一个Scope内只初始化一个实例 ,可以理解为(每一个 request 级别只创建一个实例)

(3).Transient: 每一次 GetService 都会创建一个新的实例。


注:默认是Scoped,即单次请求内是单例的。

可以自行配置:

通过查源码可知,默认就是Scoped的。

View Code


2. 测试案例

在EF上下文的构造函数中生成一个Guid,如果是单例的,构造函数只会被调用一次,我们通过比较两个上下文是否相等 或者 比较单次、多次请求中Guid的值是否相同来验证依赖注入的上下文的各种生命周期。

复制代码

 1         public EFDB01Context context1;
 2         public EFDB01Context context2;
 3         public FirstController(EFDB01Context db1, EFDB01Context db2)
 4         {
 5             context1 = db1;
 6             context2 = db2;
 7         }
 8         /// <summary>
 9         /// 生命周期测试
10         /// </summary>
11         public IActionResult TestLifeTime()
12         {
13             //测试方法一(判断两个上下文是否完全相同)
14             bool isSame1 = object.ReferenceEquals(context1,context2);
15             ViewBag.isSame = isSame1;
16 
17             //测试方法二(通过多次请求来判断Guid值是否相等)
18             ViewBag.MyGuid1 = context1.myGuid;
19             ViewBag.MyGuid2 = context2.myGuid;
20 
21             return View();
22         }

复制代码

(1).将参数设置为Singleton: 每次请求的context1和context2的值相等,单次请求和多次请求MyGuid1、MyGuid2的值均相等,从而证明是全局单例的。

(2).将参数设置为Scoped:每次请求的context1和context2的值相等,单次请求中MyGuid1和MyGuid2的值相等,多次请求的情况下,每次产生的MyGuid1之间均不相同, 每次产生的MyGuid2之间均不相同,从而证明是请求内单例的。

(3).将参数设置为Transient:每次请求的context1和context2的值不相等,单次请求中MyGuid1和MyGuid2的值也不相等,多次请求的情况下,每次产生的MyGuid1之间均不相同, 每次产生的MyGuid2之间均不相同,从而证明是瞬时的。



版权声明:本文为ysq5202121原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。