实现思路:

方案一:

一个数据库使用一个上下文Context,读写的时候,手动切换EF Core上下文的连接,这种方式虽然能实现,但是由于要频繁切换上下文,并且要先创建上下文,再关闭之前的连接,才能进行切换,如此的话性能的损耗是比较严重的。还有一个问题就是,数据库读写分离后其实是无法确定从库的数量的,可能随着业务的增长,从数据库可能会持续增加,那么增加一个从数据库,就需要增加一个Context上下文,这样后期是不利于项目的扩展和维护

方案二:

项目只使用一个DBContext,避免数据库连接的切换,相对的是直接传入相应的数据库链接字符串即可,后期如果需要新增从库,我们只需要配置对应的数据库连接字符串即可,无需对代码进行重新的维护

 

具体实现:

因为需要要指定  增删改 是对主库进行操作,查询操作是从库操作,所以我们首先需要新增一个数据库操作类(BaseDBService ,实现 IBaseDBService 接口),进行EF原生增删改查的封装,

在新增改的操作中,指定主库链接,在查询的方法中,进行指定从库的链接(由于进行了封装,我们还可以实现其他额外的方法,比如 自动 SaveChage,指定字段update 等方法)

using Microsoft.EntityFrameworkCore;
using Pomelo.Entity.Extend;
using Pomelo.Interface.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace Pomelo.Service.Base
{
    public class BaseDBService : IBaseDBService, IDisposable            
    {
        protected DbContext _Context { get; set; }
        protected IDbContextFactory _IDbContextFactory { get; set; }

        public BaseDBService(IDbContextFactory DbContextFactory)
        {
            _IDbContextFactory = DbContextFactory;
        }

        #region 查询 
        public T FirstOrDefault(object id, WriteAndRead writeAndReadEnum = WriteAndRead.Read) where T : class
        {
            //在这里应该判断究竟使用主库 还是从库的链接
            _Context = _IDbContextFactory.ConnWriteOrRead(writeAndReadEnum); 
            return this._Context.Set().Find(id);
        } 

        public T FirstOrDefault(Expression<Func<T, bool>> predicate, WriteAndRead writeAndReadEnum = WriteAndRead.Read) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(writeAndReadEnum); 
            return this._Context.Set().FirstOrDefault(predicate);
        }

        public IQueryable Where(Expression<Func<T, bool>> predicate, WriteAndRead writeAndReadEnum = WriteAndRead.Read) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(writeAndReadEnum); 
            return this._Context.Set().Where(predicate);
        }

        public int Count(Expression<Func<T, bool>> predicate, WriteAndRead writeAndReadEnum = WriteAndRead.Read) where T : class
        { 
            _Context = _IDbContextFactory.ConnWriteOrRead(writeAndReadEnum);
            return this._Context.Set().Count(predicate);
        }
        #endregion

        #region 新增
        public T Add(T t, bool autoSave = true) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);
            this._Context.Set().Add(t);
            if (autoSave)
            {
                this.SaveChanges(); 
            }
            return t;
        }

        public void AddRange(IEnumerable entities, bool autoSave = true) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);
            this._Context.Set().AddRange(entities);
            if (autoSave)
            {
                this.SaveChanges();//写在这里  就不需要单独commit  不写就需要 
            }
        }
        #endregion


        #region 更新 
        public T Update(T t, bool autoSave = true) where T : class
        {
            //告诉EF Core开始跟踪实体的更改,因为调用DbContext.Attach方法后,EF Core会将实体的State值更改回EntityState.Unchanged
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);             
            _Context.Update(t);
            if (autoSave)
            {
                this.SaveChanges();//写在这里  就不需要单独commit  不写就需要 
            }
            return t;
        }

        public T Update(T t, bool autoSave = true, params Expression<Func<T, object>>[] updatedProperties) where T : class
        {
            //告诉EF Core开始跟踪实体的更改,因为调用DbContext.Attach方法后,EF Core会将实体的State值更改回EntityState.Unchanged
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);

            _Context.Attach(t);
            if (updatedProperties.Any())
            {
                foreach (var property in updatedProperties)
                {
                    //告诉EF Core实体的属性已经更改。将属性的IsModified设置为true后, 
                    //也会将实体的State值更改为EntityState.Modified, 
                    //这样就保证了下面SaveChanges的时候会将实体的属性值Update到数据库中。 
                    _Context.Entry(t).Property(property).IsModified = true;
                }
            }  
            if (autoSave)
            {
                this.SaveChanges();//写在这里  就不需要单独commit  不写就需要 
            }
            return t;
        }        
        #endregion

        #region 删除
        public void Delete(object Id, bool autoSave = true) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);
            T t = this.FirstOrDefault(Id);//也可以附加
            if (t != null)
            {
                this._Context.Set().Remove(t);
                if (autoSave)
                {
                    this.SaveChanges();
                }
            }
        }

        public void Delete(T t, bool autoSave = true) where T : class
        {
            _Context = _IDbContextFactory.ConnWriteOrRead(WriteAndRead.Write);
            if (t != null)
            {
                this._Context.Set().Attach(t);
                this._Context.Set().Remove(t);
                if (autoSave)
                {
                    this.SaveChanges();
                }
            }
        }

        #endregion

        public void SaveChanges()
        {
            this._Context.SaveChanges();
        }

        public void Dispose()
        {
            if (this._Context != null)
            {
                this._Context.Dispose();
            }
        }
    }
}

 

可以看出,每个方法都会指定好是 Read 还是 Write(对于查询数据的方法,有即时性较高的业务,我们亦可以指定 Write 主库来获取,毕竟从库的数据同步也是有延迟的,而增删改的话,则是必须主库操作),并且把实例的创建和字符串的选择处理推向一个工厂来做;工厂接受一个枚举参数,知道是使用从库还是主库,然后由工厂来读取配置文件指定究竟来使用哪个数据库来做数据操作;因为从数据库的个数是未知的,工厂还可以在选择从库(查询的)的时候,适当的做一些策略来选择(随机或者轮询等),在查询的时候,让多个从库尽可能平均的来分摊查询的操作,从而提高数据库的性能

而这个我们通过一个工厂类(DbContextFactory,实现 IDbContextFactory接口)进行实现对 DBContext 的链接进行设置

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Text;

namespace Pomelo.Entity.Extend
{
    public class DbContextFactory: IDbContextFactory
    {
        private DBConnectionModel _readAndWrite = null;
        protected DbContext _Context { get; set; }

        public DbContextFactory(DbContext context, IOptionsMonitor options)
        {
            _Context = context;
            _readAndWrite = options.CurrentValue;
        }

        public DbContext ConnWriteOrRead(WriteAndRead writeAndRead)
        {
            switch (writeAndRead)
            {
                case WriteAndRead.Write:
                    //指定主库连接
                    ToWrite();
                    break;
                case WriteAndRead.Read:
                    //指定从库连接
                    ToRead();
                    break;
                default:
                    break;
            }
            return _Context;
        }

        //  更换成主库连接 
        private void ToWrite()
        {
            string conn = _readAndWrite.DefaultConnection; 
            _Context.ToWriteOrRead(conn);
        } 

        // 更换成从库连接 
        private void ToRead()
        {
            string conn = string.Empty;
            {
                //随机
                //也可以根据业务实现自己的从库分配策略,比如轮询等从而实现负载均衡
                int Count = _readAndWrite.SlaveConnection.Count;
                int index = new Random().Next(0, Count);
                conn = _readAndWrite.SlaveConnection[index];
            }            
            _Context.ToWriteOrRead(conn);
        }  
    } 
}

 

可以看出,DBContext 中我们还有一个扩展方法 ToWriteOrRead,其中具体实现是

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Pomelo.Entity.Extend
{
    public static class PomeloContextExtend
    {
        public static DbContext ToWriteOrRead(this DbContext dbContext, string conn)
        {
            if (dbContext is PomeloContext)
            { 
                PomeloContext context = (PomeloContext)dbContext; // context 是 PomeloContext 实例; 
                return context.ToWriteOrRead(conn);
            }
            else
                throw new Exception();
        }
    }
}

 

同样,在我们自己的数据库上下文类 PomeloContext 里面,新增一个 ToWriteOrRead 方法,将连接传入到里面进行设值

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Debug;
using Pomelo.Entity.Models;
using System.IO;

namespace Pomelo.Entity
{
    public class PomeloContext : DbContext
    {
        public static readonly LoggerFactory LoggerFactory = new LoggerFactory(new[] { new DebugLoggerProvider() });
        private string ConnectionString = string.Empty;

        public PomeloContext()
        { 
            //初始化的时候去获取主库链接,主要用于 Code First 进行迁移
            var config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
            ConnectionString = config.GetConnectionString("DefaultConnection");
        }

        public PomeloContext(DbContextOptions options)
            : base(options)
        {
        }

        override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseLoggerFactory(LoggerFactory)
                .UseMySql(ConnectionString)//使用MySQL
                //.UseSqlServer(ConnectionString)//使用SQL Server
                ; 
        }

        public DbContext ToWriteOrRead(string conn)
        {
            ConnectionString = conn;
            return this;
        } 
        public virtual DbSet Pomelo_Config { get; set; }
    }
}

 

后面如果需要增加从库,只需在配置文件里面 SlaveConnection 添加数据库连接字符串即可

 "ConnectionStrings": {
    "DefaultConnection": "Data Source=127.0.0.1;Port=3307;Initial Catalog=raw;uid=root;password=*;Charset=utf8",
    "SlaveConnection": [     
      "Data Source=127.0.0.1;Port=3309;Initial Catalog=raw;uid=root;password=*;Charset=utf8"
    ]
  }

最后,我们将数据库服务类,数据库工厂类,上下文类等在 startup类 进行注册

services.AddTransient<DbContext, PomeloContext>();
services.AddTransient<IBaseDBService, BaseDBService>();
services.AddTransient<IDbContextFactory, DbContextFactory>(); 
services.Configure(Configuration.GetSection("ConnectionStrings"));//获取配置文件的数据库连接字符串

 

最后,在控制器进行测试一下,当然,在控制器我们也要注入一下 IBaseDBService

protected readonly IBaseDBService _DBService = null;
protected BaseController(IBaseDBService dBService)
{
    _DBService = dBService;
}

public Pomelo_ConfigEntity get()
{
    var data = _DBService.FirstOrDefault(1);
    return data;
}

public Pomelo_ConfigEntity add()
{
    var data = _DBService.Add(new Pomelo_ConfigEntity
    {
       Key = "test",
       Name = DateTime.Now.ToString()
    });
    return data;
}

执行流程:进入 BaseDBService 的方法,并走到 _IDbContextFactory.ConnWriteOrRead 后进入 DbContextFactory 类获取主从库的数据库连接字符串,并在 PomeloContext  的里面设置数据库连接字符串,回到到 BaseDBService  ,在 this._Context.Set<T>().xxx 方法后,再次进入 PomeloContext 执行  OnConfiguring 方法,切换连接,进行数据库对应操作

可以看出,执行 add 的时候,连接字符串获取的是 主库 连接,执行 get 的时候,获取的是 从库 连接

 

我们执行 add 操作最终数据结果如下,可以看出,已正常实现