找回密码
 立即注册
首页 业界区 业界 【EF Core】框架底层的数据库连接管理

【EF Core】框架底层的数据库连接管理

巴沛若 2025-9-24 16:42:37
在开始水文章前,老周补充说明一下,在前一篇水文中,老周扯了跟 Owned 关系相关的一些话题,这里补充一句:Owned 关系是不存在“多对多”的,如果 A 实体 Own B 实体,那么,A 对 B 是完全占有,B只属于A,数据不共享,这样就不存在“多”的一端;但A可以同时占用B、C实体,所以一占多是存在的。说得简单一点,就是 B 都被 A 独占了,其他人还来凑啥热闹?
好了,正片开始!外面台风呼啸,雨声沥沥,很适合探讨一些轻松的话题。于是,老周就说一下 EF Core 底层对数据库连接的管控吧。其实,各位如果项目中常用 EF Core,就会发现,大多数时候我们根本不用去考虑连接数据库的事,引用数据库对应的包,添加 Provider(指 UseSql、UseSqlite 等方法的调用),传递一个连接字符串就完事了。
这是因为 EF Core 已经把数据库连接相关操作封装了。实际上它的底层还是会用到 ADO.NET 相关的 API。嗯,就是大伙都很熟悉的三件套:DbConnection、DbCommand、DbDataReader。当然,这里列出的是通过的类,还有 DbBatch、DbDataAdapter 等类,只是不完全通用,有些数据库是不提供的。这些以 Db 开头的类均位于 System.Data.Common 空间,属于公共基类,不同的数据库必须提供自身的实现类。如:
1、SQL Server:SqlConnection、SqlCommand、SqlDataReader;
2、SQLite:SqliteConnection、SqliteCommand、SqliteDataReader;
3、PostgreSQL:NpgsqlConnection、NpgsqlCommand、NpgsqlDataReader;
……
其中,SQL Server 和 PostgreSQL 是有 DataAdapter 的,分别是 SqlDataAdapter 和 NpgsqlDataAdapter。SQLite 没有提供。
这样就保证了尽管面向不同的数据库,但 API 的调用过程差不多:
A、实例化 XXXConnection;
B、创建 XXXCommand 实例,设置 SQL 语句;
C、XXXCommand 实例调用 ExecuteXXX 执行 SQL,可能不需要返回结果,也可能需要 XXXDataReader 来读取结果;
D、关闭 XXXConnection 对象。
由于各种数据库相关的连接对象都是 DbConnection 的子类,于是,在连接管理上,只要统使用 DbConnection 类型就能把连接管理抽象出来,统一描述。为了实现这个“宏大目标”,EF Core 在面向关系数据库专用包(xxxx.Relational.dll)中提供了 IRelationalConnection 接口。这个接口完成了以下规范:
1、ConnectionString 属性:通过它,你可以设置 / 获取连接字符串;

2、DbConnection 属性:这个很重要,有了此属性,就可以设置 / 获取连接对象了,定义的类型正是公共基类 DbConnection;
3、Open / OpenAsync 方法:打开连接;
4、Close / CloseAsync 方法:关闭连接;
5、RentCommand 方法:调用它,它会自动帮你创建/重用一个命令实例,用 IRelationalCommand 接口封装了。这个接口稍后再介绍;
6、ReturnCommand 方法:命令对象使用完后,可以调用这个方法,把实例仍回去,以便将来可以重复/或全新使用。框架帮你管理其内存,不用你操心。
有小伙伴会疑惑:咦,我在 EFCore 源代码中搜索 SqlCommand、SqliteCommand 等关键字,居然找不到它在哪里使用命令。你不要忘了,DbConnection 类有个叫 CreateCommand 的方法,所有派生类都实现这个方法。SqlConnection 类会让它返回 SqlCommand 实例,SqliteConnection 类会让它返回 SqliteCommand 实例。但由于 CreateCommand 方法定义的返回类型是 DbCommand 类,因此它有通用性。EF Core 中就是调用了 CreateCommand 方法来获得命令实例的。这就不得不提前文中出现的一个接口了—— IRelationalCommand。它统一了一些方法:
1、ExecuteNonQuery / ExecuteNonQueryAsync 方法:执行命令,通常不返回查询结果,如 INSERT、DELETE 等;
2、ExecuteReader / ExecuteReaderAsync 方法:执行后会有查询结果,但 XXXDataReader 类被 RelationalDataReader 类封装了;
3、ExecuteScalar / ExecuteScalarAsync 方法:返回单个值。
IRelationalCommand 接口的实现类是 RelationalCommand。它有个 CreateDbCommand 公共方法。
  1.     public virtual DbCommand CreateDbCommand(
  2.         RelationalCommandParameterObject parameterObject,
  3.         Guid commandId,
  4.         DbCommandMethod commandMethod)
  5.     {
  6.         var (connection, context, logger) = (parameterObject.Connection, parameterObject.Context, parameterObject.Logger);
  7.         var connectionId = connection.ConnectionId;
  8.         var startTime = DateTimeOffset.UtcNow;
  9.         DbCommand command;
  10.         var stopwatch = SharedStopwatch.StartNew();
  11.         var logCommandCreate = logger?.ShouldLogCommandCreate(startTime) == true;
  12.         if (logCommandCreate)
  13.         {
  14.             var interceptionResult = logger!.CommandCreating(
  15.                 connection, commandMethod, context, commandId, connectionId, startTime,
  16.                 parameterObject.CommandSource);
  17.             command = interceptionResult.HasResult
  18.                 ? interceptionResult.Result
  19.                 : <strong>connection.DbConnection.CreateCommand()</strong>;
  20.             command = logger.CommandCreated(
  21.                 connection,
  22.                 command,
  23.                 commandMethod,
  24.                 context,
  25.                 commandId,
  26.                 connectionId,
  27.                 startTime,
  28.                 stopwatch.Elapsed,
  29.                 parameterObject.CommandSource);
  30.         }
  31.         else
  32.         {
  33.             <strong>command </strong><strong>=</strong><strong> connection.DbConnection.CreateCommand()</strong>;
  34.         }
  35.         command.CommandText = CommandText;
  36.         if (connection.CurrentTransaction != null)
  37.         {
  38.             command.Transaction = connection.CurrentTransaction.GetDbTransaction();
  39.         }
  40.         if (connection.CommandTimeout != null)
  41.         {
  42.             command.CommandTimeout = (int)connection.CommandTimeout;
  43.         }
  44.         for (var i = 0; i < Parameters.Count; i++)
  45.         {
  46.             Parameters[i].AddDbParameter(command, parameterObject.ParameterValues);
  47.         }
  48.         if (logCommandCreate)
  49.         {
  50.             command = logger!.CommandInitialized(
  51.                 connection,
  52.                 command,
  53.                 commandMethod,
  54.                 context,
  55.                 commandId,
  56.                 connectionId,
  57.                 startTime,
  58.                 stopwatch.Elapsed,
  59.                 parameterObject.CommandSource);
  60.         }
  61.         <strong>return</strong><strong> command</strong>;
  62.     }
复制代码
 
好了,现在基本的原理通了,咱们回到 IRelationalConnection 接口,它有一个抽象类实现:RelationalConnection。这个类中定义了一个抽象方法叫 CreateDbConnection,各种数据库在匹配 API 时会重写此方法。比如:
A、SQLite 数据库提供者,从 RelationalConnection 派生出 SqliteRelationalConnection 类,重写 CreateDbConnection 方法。
  1. protected override DbConnection CreateDbConnection()
  2. {
  3.     var connection = new SqliteConnection(GetValidatedConnectionString());
  4.     InitializeDbConnection(connection);
  5.     return connection;
  6. }
复制代码
B、SQL Server 数据提供者:从 RelationalConnection 派生出 SqlServerConnection 类,重写 CreateDbConnection 方法。
  1. protected override DbConnection CreateDbConnection()
  2.     => new SqlConnection(GetValidatedConnectionString());
复制代码
C、PostgreSQL 数据库提供者:从 RelationalConnection 派生出 NpgsqlRelationalConnection 类,重写 CreateDbConnection 方法。
  1.     protected override DbConnection CreateDbConnection()
  2.     {
  3.         if (DataSource is not null)
  4.         {
  5.             return DataSource.CreateConnection();
  6.         }
  7.         var conn = new NpgsqlConnection(ConnectionString);
  8.         if (_provideClientCertificatesCallback is not null || _remoteCertificateValidationCallback is not null)
  9.         {
  10.             conn.SslClientAuthenticationOptionsCallback = o =>
  11.             {
  12.                 if (_provideClientCertificatesCallback is not null)
  13.                 {
  14.                     o.ClientCertificates ??= new();
  15.                     _provideClientCertificatesCallback(o.ClientCertificates);
  16.                 }
  17.                 o.RemoteCertificateValidationCallback = _remoteCertificateValidationCallback;
  18.             };
  19.         }
  20.         if (_providePasswordCallback is not null)
  21.         {
  22. #pragma warning disable 618 // ProvidePasswordCallback is obsolete
  23.             conn.ProvidePasswordCallback = _providePasswordCallback;
  24. #pragma warning restore 618
  25.         }
  26.         return conn;
  27.     }
复制代码
 
每个数据库提供者都会把实现 IRelationalConnection 接口的类注册到服务容器中,也就是说,咱们在应用代码中是可以访问此接口的功能的。
  1. // 构建连接字符串
  2. SqliteConnectionStringBuilder csbuilder = new();
  3. csbuilder.DataSource = "test.db";
  4. csbuilder.Password = "huhuhahe";
  5. // 构建选项
  6. DbContextOptions<DbContext> options = new DbContextOptionsBuilder<DbContext>()
  7.     .UseSqlite(csbuilder.ConnectionString)
  8.     .Options;
  9. // 此处只用来测试 IRelationalConnection 服务的访问、
  10. // 所以无实体类无 DbContext 的派生类
  11. using DbContext context = new(options);
  12. // 获取服务
  13. IRelationalConnection conn = <strong>context.GetService<IRelationalConnection></strong><strong>()</strong>;
  14. // 打印连接字符串
  15. Console.WriteLine($"连接字符串:{<strong>conn.ConnectionString</strong>}");
复制代码
代码运行后,输出结果如下:
  1. 连接字符串:Data Source=test.db;Password=huhuhahe
复制代码
 
------------------------------------------------------------------------------------------------------------------------------------------------------------
上面的示例其实没啥鸟用,接下来老周讲个比较有实用性的。下面咱们看看共享 DbConnection。
实体和 Context 如下:
  1. public class Dog
  2. {
  3.     public Guid DogId { get; set; }
  4.     public string Name { get; set; } = "Who?";
  5.     public int Age { get; set; }
  6.     public string? Category { get; set; }
  7. }
  8. public class MyDbContext : DbContext
  9. {
  10.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  11.     {
  12.         EntityTypeBuilder<Dog> tb = modelBuilder.Entity<Dog>();
  13.         tb.HasKey(d => d.DogId).HasName("PK_Dog");
  14.         tb.ToTable("tb_dogs");
  15.         tb.Property(x => x.Name).HasMaxLength(20).IsRequired();
  16.         tb.Property(a => a.Category).HasDefaultValue("未确认");
  17.     }
  18.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  19.     {
  20.         <strong>optionsBuilder.UseSqlite(_connection)</strong>;
  21.     }
  22.     DbConnection _connection;
  23.     public MyDbContext(<strong>DbConnection c</strong>)
  24.     {
  25.         _connection = c;
  26.     }
  27.     public DbSet<Dog> DogSet { get; set; }
  28. }
复制代码
注意这个 MyDbContext 类,它的构造函数可以传递 DbConnection 对象,然后在重写的 OnConfiguring 方法中调用 UseSqlite 扩展方法直接引用外部的 DbConnection 对象。这样就实现了连接对象的共享。接着看代码:
  1. static void Main(string[] args)
  2. {
  3.      <strong>SqliteConnection myconn </strong><strong>= new("data source=mme.db"</strong><strong>)</strong>;
  4.      // 初始化数据库
  5.      using (MyDbContext ctx = new(myconn))
  6.      {
  7.          ctx.Database.EnsureDeleted();
  8.          ctx.Database.EnsureCreated();
  9.      }
  10.      // 插入数据
  11.      using (var ctx = new MyDbContext(myconn))
  12.      {
  13.          ctx.DogSet.Add(new()
  14.          {
  15.              Name = "小菜",
  16.              Age = 2,
  17.              Category = "吉娃娃"
  18.          });
  19.          ctx.DogSet.Add(new()
  20.          {
  21.              Name = "Jimy",
  22.              Age = 3,
  23.              Category = "贵宾犬"
  24.          });
  25.          ctx.SaveChanges();
  26.      }
  27.      // 查询数据
  28.      using (MyDbContext c = new MyDbContext(myconn))
  29.      {
  30.          foreach (Dog d in c.DogSet)
  31.          {
  32.              Console.WriteLine($"{d.Name} - {d.Category}, {d.Age}岁
  33. ");
  34.          }
  35.      }
  36. }
复制代码
先实例化连接对象,然后依次传递三个 MyDbContext 实例使用。
好了,今天咱们就水到这里吧。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册