前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于EntityFramework 6 Code First实现多租户的一种思路

基于EntityFramework 6 Code First实现多租户的一种思路

作者头像
xhznl
发布2020-08-14 11:24:42
5290
发布2020-08-14 11:24:42
举报
文章被收录于专栏:小黑在哪里小黑在哪里

一、前言

公司原本有一个“xx系统”,ORM使用EntityFramework,Code First模式。该系统是针对某个客户企业的,现要求该系统支持多个企业使用,但是又不能给每个企业部署一份(难以维护),只能想办法从代码层面去解决这个问题。

二、思路

  1. 在原有的数据表增加外键,标记该数据属于哪个企业。这代码改动会非常大,之前的查询修改代码都需要增加外键筛选的逻辑。这显然不合理。
  2. 动态分库。每个企业注册时,为他生成一个独立的数据库,企业登录时切换到他对应的数据库。这样就完全不用修改以前的业务代码,只需要考虑企业数据库切换的问题。

三、实现

那么EntityFramework Code First模式怎么实现动态分库的功能呢?

  1. 首先建立一个主库,主库只存放企业用户的数据,包括企业登录名,密码,对应的数据库名 等等... 主库只有一个。
  2. 业务数据库,在企业注册的时候动态创建,业务数据库可以有多个,也可以放到不同的服务器。
  3. 企业登录时,读取主库,拿到业务数据库名称,然后保存到用户session中(也可以是别的缓存),该用户的后续请求都基于此数据库。

为了简单我建立了一个demo项目:

主库模型放在XHZNL.EFDynamicDatabaseBuilding.MasterEntity里面,主库只有一个企业表:Enterprise:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XHZNL.EFDynamicDatabaseBuilding.MasterEntity
{
    /// <summary>
    /// 企业
    /// </summary>
    public class Enterprise
    {
        /// <summary>
        /// ID
        /// </summary>
        [Required]
        public Guid ID { get; set; }

        /// <summary>
        /// 企业名称
        /// </summary>
        [Required]
        [Column(TypeName = "NVARCHAR")]
        [MaxLength(50)]
        public string Name { get; set; }

        /// <summary>
        /// 企业数据库名称
        /// </summary>
        [Required]
        [Column(TypeName = "NVARCHAR")]
        [MaxLength(100)]
        public string DBName { get; set; }

        /// <summary>
        /// 企业 账号
        /// </summary>
        [Required]
        [Column(TypeName = "NVARCHAR")]
        [MaxLength(20)]
        public string AdminAccount { get; set; }

        /// <summary>
        /// 企业 密码
        /// </summary>
        [Required]
        [Column(TypeName = "NVARCHAR")]
        [MaxLength(50)]
        public string AdminPassword { get; set; }
    }
}

XHZNL.EFDynamicDatabaseBuilding.MasterEntity.Services.BaseService:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XHZNL.EFDynamicDatabaseBuilding.Common;

namespace XHZNL.EFDynamicDatabaseBuilding.MasterEntity.Services
{
    public class BaseService
    {
        /// <summary>
        /// 获取context
        /// </summary>
        /// <returns></returns>
        internal MasterDBContext GetDBContext()
        {
            try
            {
                var context = new MasterDBContext();

                if (!context.Database.Exists())
                {
                    context.Database.Create();

                    var dbInitializer = new MigrateDatabaseToLatestVersion<MasterDBContext, Migrations.Configuration>(true);
                    dbInitializer.InitializeDatabase(context);
                }

                if (!context.Database.CompatibleWithModel(false))
                {
                    var dbInitializer = new MigrateDatabaseToLatestVersion<MasterDBContext, Migrations.Configuration>(true);
                    dbInitializer.InitializeDatabase(context);
                }

                return context;
            }
            catch (Exception ex)
            {
                return null;
            }
        }
    }
}

XHZNL.EFDynamicDatabaseBuilding.MasterEntity.Services.EnterpriseService:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XHZNL.EFDynamicDatabaseBuilding.Common;

namespace XHZNL.EFDynamicDatabaseBuilding.MasterEntity.Services
{
    /// <summary>
    /// 企业服务
    /// </summary>
    public class EnterpriseService : BaseService
    {
        public static readonly EnterpriseService Instance = new EnterpriseService();

        private EnterpriseService() { }

        /// <summary>
        /// 根据账号密码 获取 企业
        /// </summary>
        /// <param name="account"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public Enterprise Get(string account, string password)
        {
            try
            {
                using (var context = GetDBContext())
                {
                    var model = context.Enterprises.FirstOrDefault(m => m.AdminAccount == account && m.AdminPassword == password);
                    if (model != null)
                    {
                        //设置当前业务数据库
                        CommonHelper.Instance.SetCurrentDBName(model.DBName);
                    }
                    return model;
                }
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        /// <summary>
        /// 添加企业
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public bool Add(Enterprise enterprise)
        {
            try
            {
                using (var context = GetDBContext())
                {
                    enterprise.ID = Guid.NewGuid();
                    enterprise.DBName = "BusinessDB" + DateTime.Now.Ticks;
                    context.Enterprises.Add(enterprise);
                    return context.SaveChanges() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }
        }

    }
}

XHZNL.EFDynamicDatabaseBuilding.Common.CommonHelper:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;

namespace XHZNL.EFDynamicDatabaseBuilding.Common
{
    public class CommonHelper
    {
        public static readonly CommonHelper Instance = new CommonHelper();

        private CommonHelper() { }


        /// <summary>
        /// 获取当前数据库
        /// </summary>
        /// <returns></returns>
        public string GetCurrentDBName()
        {
            var key = "CurrentDBName";

            string name = null;

            if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Session != null)
            {
                name = System.Web.HttpContext.Current.Session[key].ToString();
            }
            else
            {
                name = CallContext.GetData(key).ToString();
            }

            if (string.IsNullOrEmpty(name))
                throw new Exception("CurrentDBName异常");

            return name;
        }

        /// <summary>
        /// 设置当前数据库
        /// </summary>
        /// <param name="name"></param>
        public void SetCurrentDBName(string name)
        {
            var key = "CurrentDBName";

            if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Session != null)
            {
                System.Web.HttpContext.Current.Session[key] = name;
            }
            else
            {
                CallContext.SetData(key, name);
            }
        }
    }
}

web.config配置一下业务数据库的连接信息:

这个可以根据实际业务修改,分布到不同的服务器,这里只是为了演示。

业务数据库模型放在XHZNL.EFDynamicDatabaseBuilding.BusinessEntity里面,这里只有一个员工表

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XHZNL.EFDynamicDatabaseBuilding.BusinessEntity
{
    /// <summary>
    /// 员工
    /// </summary>
    public class Staff
    {
        /// <summary>
        /// ID
        /// </summary>
        [Required]
        public Guid ID { get; set; }

        /// <summary>
        /// 员工名称
        /// </summary>
        [Required]
        [Column(TypeName = "NVARCHAR")]
        [MaxLength(50)]
        public string Name { get; set; }
    }
}

数据库context:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XHZNL.EFDynamicDatabaseBuilding.BusinessEntity
{

    //[DbConfigurationType(typeof(MySql.Data.Entity.MySqlEFConfiguration))]//使用mysql时需要这个
    internal class BusinessDBContext : DbContext
    {
        public BusinessDBContext() : base("name=BusinessDB")
        {
            Database.SetInitializer<BusinessDBContext>(null);
        }

        //修改上下文默认构造函数  
        public BusinessDBContext(string connectionString)
            : base(connectionString)
        {

        }

        /// <summary>
        /// 员工
        /// </summary>
        public DbSet<Staff> Staffs { get; set; }
    }
}

XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.Migrations.Configuration:可以放一些种子数据...

代码语言:javascript
复制
namespace XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.BusinessDBContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
            //SetSqlGenerator("MySql.Data.MySqlClient", new MySql.Data.Entity.MySqlMigrationSqlGenerator());//使用mysql时需要这个
        }

        protected override void Seed(XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.BusinessDBContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method
            //  to avoid creating duplicate seed data.

            var staff = new Staff() { ID = Guid.Parse("212cf53c-6801-4c00-b36b-996ac9809e04"), Name = "初始员工" };
            context.Staffs.AddOrUpdate(staff);
            context.SaveChanges();
        }
    }
}

关键的分库,建库,更新数据库代码在XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.Services.BaseService,任何的模型修改都能在程序运行时自动更新到数据库:

代码语言:javascript
复制
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using XHZNL.EFDynamicDatabaseBuilding.Common;

namespace XHZNL.EFDynamicDatabaseBuilding.BusinessEntity.Services
{
    public class BaseService
    {
        /// <summary>
        /// 获取context
        /// </summary>
        /// <returns></returns>
        internal BusinessDBContext GetDBContext()
        {
            try
            {
                //mysql连接字符串
                //var connectionString = $"Data Source={AppConfig.DB_DataSource};Port={AppConfig.DB_Port};Initial Catalog={CommonHelper.Instance.GetCurrentDBName()};User ID={AppConfig.DB_UserID};Password={AppConfig.DB_Password};";

                //sqlserver连接字符串
                var connectionString = $"Data Source={AppConfig.DB_DataSource},{AppConfig.DB_Port};Initial Catalog={CommonHelper.Instance.GetCurrentDBName()};User ID={AppConfig.DB_UserID};Password={AppConfig.DB_Password};";

                var context = new BusinessDBContext(connectionString);

                //数据库是否存在 不存在则创建
                if (!context.Database.Exists())
                {
                    context.Database.Create();

                    var dbInitializer = new MigrateDatabaseToLatestVersion<BusinessDBContext, Migrations.Configuration>(true);
                    dbInitializer.InitializeDatabase(context);
                }

                //数据库接口是否和模型一致 不一致则更新
                if (!context.Database.CompatibleWithModel(false))
                {
                    var dbInitializer = new MigrateDatabaseToLatestVersion<BusinessDBContext, Migrations.Configuration>(true);
                    dbInitializer.InitializeDatabase(context);
                }

                return context;
            }
            catch (Exception ex)
            {
                return null;
            }
        }
    }
}

其他的数据访问类继承BaseService,通过GetDBContext()方法获取context,这样确保得到正确的业务数据库。

四、效果

  • 运行web项目:

此时数据库中只有一个主库:

  • 点击注册企业:

注册2个企业用于测试 此时主库已有了2条企业数据:

  • 分别用test1,test2登录,并添加员工数据:

企业登录后已经生成了对应的业务库

  • 数据正确添加读取:

五、总结:

以上关于EntityFramework分库的核心就是通过动态构建connectionString,来得到context。至于如何动态构建,方法有很多,以上代码只是最简单的实现。代码在:https://github.com/xiajingren/EFDynamicDatabaseBuilding

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小黑在哪里 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、思路
  • 三、实现
  • 四、效果
  • 五、总结:
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档