這篇博客描述的是運行環境是.Net 7下使用WebApi,ORM框架使用EF Core的DbFirst模式,再配合上SqlServer的1主,2從3個數據庫,完成的讀寫分離封裝。
一.先準備3個數據庫,1主,2從
我先準備了3個數據庫,分別是:SchoolDB(作爲主庫,到時候只負責寫)、SchoolDB_Read_1(作爲從庫1,到時候只負責讀)、SchoolDB_Read_2(作爲從庫2,到時候只負責讀),裏面都有張學生表。
二.EFCore DbFirst模式生成實體和DbContext
根據數據庫生成實體,工具=>NuGet包管理器=>程序包管理器控制檯(項目設置爲啓動項)
生成命令:
三.封裝前的其他類準備
3.1.數據庫連接配置
namespace MengLin.Shopping.SchoolDB.DbFirst.ConfigureOptions { /// <summary> /// 數據庫連接配置 /// </summary> public class ConnectionStringOptions { /// <summary> /// 寫鏈接-主庫Mast /// </summary> public string WriteConnection { get; set; } /// <summary> /// 讀鏈接-從庫Salve /// </summary> public List<string> ReadConnectionList { get; set; } } }
3.2.appsettings.json映射ConnectionStringOptions數據庫連接配置的json文件
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStringOptions": { "WriteConnection": "Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true", "ReadConnectionList": [ "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_1;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true", "Server=meng\\MSSQLSERVERML;Database=SchoolDB_Read_2;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true" ] } }
3.3.操作數據庫是讀還是寫的枚舉
namespace MengLin.Shopping.SchoolDB.DbFirst.Enum { public enum WriteAndReadEnum { //主庫操作 Write, //從庫操作 Read } }
四.定義接口-IBaseService
定義一些常用、共性操作,比如:增刪改查,爲了簡化代碼,我這裏只是定義了Insert添加、Where分頁查詢、Commit提交三個方法。
/// <summary> /// 基本操作接口 /// </summary> public interface IBaseService { /// <summary> /// 添加數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <returns></returns> T Insert<T>(T t) where T : class;
/// <summary> /// 根據表達式目錄樹進行分頁查找數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表達式目錄樹</param> /// <param name="writeAndReadEnum">默認從庫操作</param> /// <returns></returns> (IQueryable<T>, int totalCount) Where<T>(Expression<Func<T, bool>> expression,int pageIndex,int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class; /// <summary> /// 保存提交 /// </summary> void Commit(); }
五.定義基本操作實現類-BaseService
繼承IBaseService接口,實現接口裏面的Insert添加、Where分頁查詢、Commit提交這三個方法。
/// <summary> /// 基本操作實現 /// </summary> public class BaseService : IBaseService, IDisposable { /// <summary> /// 數據訪問工廠 /// </summary> private DBContextFactory _dbContextFactory = null; /// <summary> /// 構造函數注入DbContext工廠 /// </summary> /// <param name="dbContext"></param> public BaseService(DBContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; } /// <summary> /// 插入數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key">主鍵</param> /// <returns></returns> public T Insert<T>(T t) where T : class {
//只能對主庫增加 _dbContext = _dbContextFactory.GetSetupDbContext(WriteAndReadEnum.Write); _dbContext.Set<T>().Add(t); return t; }
/// <summary> /// 保存提交 /// </summary> public void Commit() { _dbContext.SaveChanges(); } /// <summary> /// 釋放資源 /// </summary> public void Dispose() { if (_dbContext != null) { _dbContext.Dispose(); } }
/// <summary> /// 根據表達式目錄樹進行分頁查找數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="expression">表達式目錄樹</param> /// <param name="writeAndReadEnum">默認從庫操作</param> /// <returns></returns> public (IQueryable<T>,int totalCount) Where<T>(Expression<Func<T, bool>> expression, int pageIndex, int pageSize, WriteAndReadEnum writeAndReadEnum = WriteAndReadEnum.Read) where T : class { //選擇其中一個從庫進行查詢 _dbContext = _dbContextFactory.GetSetupDbContext(writeAndReadEnum); return (_dbContext.Set<T>().Where(expression).Skip((pageIndex-1) * pageSize).Take(pageSize), _dbContext.Set<T>().Where(expression).Count()); } }
六.定義學生服務類-IStudentService、StudentService
學生服務,除了基本的增刪改查,還要有自己的行爲,比如:打遊戲,學習。
/// <summary> /// 學生接口 /// </summary> public interface IStudentService:IBaseService { /// <summary> /// 學習 /// </summary> public void Study(); /// <summary> /// 玩遊戲 /// </summary> public void PalyGame(); }
/// <summary> /// 學生服務 /// </summary> public class StudentService:BaseService, IStudentService { public StudentService(DBContextFactory dbContextFactory) : base(dbContextFactory) { } /// <summary> /// 學習 /// </summary> public void Study() { Console.WriteLine("我要學習了!"); } /// <summary> /// 玩遊戲 /// </summary> public void PalyGame() { Console.WriteLine("我要玩遊戲了!"); } }
七.最重要的來了,DBContext工廠類-DBContextFactory
.Net 7框架中動不動來個工廠,比如:DefaultServiceProviderFactory(IOC容器工廠,造容器的)、DefaultControllerFactory(控制器工廠,造控制器的),我也借鑑.Net 7框架的思想,我來個DBContextFactory,造DBContext的,其實也不算造DBContext,只是指定DBContext的數據庫連接字符串。
在DBContextFactory中完成了對DBContext連接數據庫字符串的指定。
namespace MengLin.Shopping.SchoolDB.DbFirst.Factory { /// <summary> /// DBContext製造工廠 /// </summary> public class DBContextFactory { /// <summary> /// DbContext數據庫上下文 /// </summary> private readonly DbContext _dbContext = null; /// <summary> /// 讀/寫數據庫連接字符串配置 /// </summary> private readonly ConnectionStringOptions _connectionStringOptions = null; /// <summary> /// 構造函數注入DbContext實例 /// 構造函數Option注入讀/寫數據庫連接字符串配置 /// </summary> /// <param name="dbContext"></param> public DBContextFactory(DbContext dbContext, IOptionsSnapshot<ConnectionStringOptions> connectionStringOptions) { _dbContext = dbContext; _connectionStringOptions = connectionStringOptions.Value; } /// <summary> /// 得到已經重新設置過數據庫連接的DbContext /// </summary> /// <param name="writeAndReadEnum">標記讀或寫</param> /// <returns></returns> public DbContext GetSetupDbContext(WriteAndReadEnum writeAndReadEnum) { //設置讀數據庫連接字符串 if (writeAndReadEnum is WriteAndReadEnum.Read) { SetReadConnectionString(); } else if(writeAndReadEnum is WriteAndReadEnum.Write)//設置寫數據庫連接字符串 { SetWriteConnectionString(); } return _dbContext; } /// <summary> /// 設置寫數據庫連接字符串 /// </summary> private void SetWriteConnectionString() { //從注入的Options配置中獲取寫的數據庫鏈接字符串 string writeConnectionString = _connectionStringOptions.WriteConnection; if (_dbContext is SchoolDBContext) { var schoolDBContext = (SchoolDBContext)_dbContext; schoolDBContext.SetWriteOrReadConnectionString(writeConnectionString); } } private static int seed = 0;//種子 /// <summary> /// 設置讀數據庫連接字符串 /// </summary> private void SetReadConnectionString() {//隨機策略--取得讀的數據庫鏈接字符串 //int connectionStringCount = _connectionStringsOptions.ReadConnectionList.Count; //int index = new Random().Next(0, connectionStringCount); //string readConnectionString = _connectionStringsOptions.ReadConnectionList[index]; //均衡策略---第1次index爲0,第2次index爲1,第3次index爲0 // 第4次index爲1,第5次index爲0,第6次index爲1 // 第7次index爲0,第8次index爲1,第9次index爲0 // 0 % 2 = 0 1 % 2 = 1 2 % 2 = 0 // 3 % 2 = 1 4 % 2 = 0 5 % 2 = 1 // 6 % 2 = 0 7 % 2 = 1 8 % 2 = 0 int connectionStringCount = _connectionStringOptions.ReadConnectionList.Count; int index = seed++ % connectionStringCount; string readConnectionString = _connectionStringOptions.ReadConnectionList[index];//索引不是0就是1 if (_dbContext is SchoolDBContext) { var schoolDBContext = (SchoolDBContext)_dbContext; schoolDBContext.SetWriteOrReadConnectionString(readConnectionString); } } } }
八.SchoolDBContext
設置它自己的訪問數據庫的連接字符串。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { //這裏寫死了 //optionsBuilder.UseSqlServer("Server=meng\\MSSQLSERVERML;Database=SchoolDB;uid=sa;pwd=123456abc;Trusted_Connection=True;TrustServerCertificate=true"); //動態使用數據庫鏈接字符串 optionsBuilder.UseSqlServer(connectionString); } } //數據庫鏈接字符串 private string connectionString = string.Empty; /// <summary> /// 設置讀或者寫的數據庫鏈接字符串 /// </summary> /// <param name="connString">鏈接字符串</param> public void SetWriteOrReadConnectionString(string connString) { connectionString = connString; }
九.程序入口Program中註冊數據庫上下文類(SchoolDBContext)、數據庫上下文工廠類(DBContextFactory)、學生服務類(StudentService)
public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); #region 註冊服務,以及數據庫上下文工廠 { //註冊數據庫上下文類 builder.Services.AddScoped<DbContext, SchoolDBContext>(); //注註冊數據庫上下文工廠類-目的:爲了修改數據庫上下文類的數據庫連接字符串 builder.Services.AddScoped<DBContextFactory, DBContextFactory>(); //註冊學生服務 builder.Services.AddScoped<IStudentService, StudentService>(); } #endregion #region 註冊配置 { //註冊配置實例到哪個TOptions builder.Services.Configure<ConnectionStringOptions>(builder.Configuration.GetSection("ConnectionStringOptions")); } #endregion //添加跨越策略 builder.Services.AddCors(options => options.AddPolicy("any", policy => { //設定允許跨域的來源,有多個可以用','隔開 policy.WithOrigins("http://localhost:8080", "http://localhost:8080") .AllowAnyHeader()//允許任何標頭 .AllowAnyMethod()//允許任何方法訪問 .AllowCredentials();//允許憑據的策略 })); builder.Services.AddSwaggerGen(s => { s.SwaggerDoc("V1", new OpenApiInfo() { Title = "通用後臺系統", Version = "Version-01", Description = "通用後臺系統" }); var currentDirectory = AppContext.BaseDirectory; s.IncludeXmlComments($"{currentDirectory}/MengLin.DotNet7.WebAPI.xml"); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(s => { s.SwaggerEndpoint("/swagger/V1/swagger.json", "test1"); }); } //使用跨越策略 app.UseCors("any"); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } }
十.訪問學生的控制器
在StudentService服務去做查詢的時候,內部設置了下SchoolDBContext連接的字符串爲讀庫的連接字符串。
namespace MengLin.DotNet7.WebAPI.Controllers { /// <summary> /// 學生 /// </summary> [Route("api/[controller]")] [ApiController] public class StudentsController : ControllerBase { /// <summary> /// 獲取學生列表 /// </summary> /// <returns></returns> [HttpGet] [Route("{pageindex}/{pagesize}")] public RespResult Get(IStudentService studentService) { //從請求中獲取路由數據 RouteValueDictionary dicRouteValue = HttpContext.Request.RouteValues; //url中的頁索引和頁大小 int.TryParse(dicRouteValue["pageindex"]?.ToString(),out int pageIndex); int.TryParse(dicRouteValue["pageSize"]?.ToString(), out int pageSize); var respResult = new RespResult<IQueryable<Student>>(); //查詢滿足條件的學生,且帶分頁 (IQueryable<Student> studentList,int totalCount) = studentService.Where<Student>(c => c.Sex == "女", pageIndex, pageSize); respResult.data = studentList;//結果集 respResult.TotalCount = totalCount;//記錄總條數 return respResult; } } }
最後附上一張項目圖