基于EntityFramework 6 Code First实现动态建库,分库,数据库自动迁移
一、前言
公司原本有一个“xx系统”,ORM使用EntityFramework,Code First模式。该系统是针对某个客户企业的,现要求该系统支持多个企业使用,但是又不能给每个企业部署一份(难以维护),只能想办法从代码层面去解决这个问题。
二、思路
- 在原有的数据表增加外键,标记该数据属于哪个企业。这代码改动会非常大,之前的查询修改代码都需要增加外键筛选的逻辑。这显然不合理。
- 动态分库。每个企业注册时,为他生成一个独立的数据库,企业登录时切换到他对应的数据库。这样就完全不用修改以前的业务代码,只需要考虑企业数据库切换的问题。
三、实现
那么EntityFramework Code First模式怎么实现动态分库的功能呢?
- 首先建立一个主库,主库只存放企业用户的数据,包括企业登录名,密码,对应的数据库名 等等... 主库只有一个。
- 业务数据库,在企业注册的时候动态创建,业务数据库可以有多个,也可以放到不同的服务器。
- 企业登录时,读取主库,拿到业务数据库名称,然后保存到用户session中(也可以是别的缓存),该用户的后续请求都基于此数据库。
为了简单我建立了一个demo项目:
主库模型放在XHZNL.EFDynamicDatabaseBuilding.MasterEntity里面,主库只有一个企业表:Enterprise:
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:
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:
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:
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里面,这里只有一个员工表
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:
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:可以放一些种子数据...
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,任何的模型修改都能在程序运行时自动更新到数据库:
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
基于EntityFramework 6 Code First实现动态建库,分库,数据库自动迁移的更多相关文章
- 【ITOO 3】.NET 动态建库建表:实用EF框架提供的codeFirst实现动态建库
导读:在上篇博客中,介绍了使用SQL字符拼接的方式,实现动态建库建表的方法.这样做虽然也能够实现效果,但是,太麻烦,而且,如果改动表结构,字段的话,会对代码修改很多.但是EF给我们提供了一种代码先行的 ...
- 使用CodeFirst实现动态建库
一.业务分析 以我们平时注册今目标为例,我们在注册今目标的过程中,具体步骤是这样的: 图1 今目标登陆流程 详细解释一下: 第一步:注册界面.输入手机号或者邮箱,点击确定进入基本信息界面. 第二步:基 ...
- 【ITOO 2】.NET 动态建库建表:使用SQL字符串拼接方式
导读:在最近接手的项目(高效云平台)中,有一个需求是要当企业用户注册时,给其动态的新建一个库和表.刚开始接手的时候,是一点头绪都没有,然后查了一些资料,也问了问上一版本的师哥师姐,终于有了点头绪.目前 ...
- [ITOO]动态建库 标签: 库数据库mysql 2016-07-17 21:23 241人阅读 评论(2) 收
最近一直在做权限系统的动态建库,动态建库,说白了就是在你点击"注册"按钮的时候,根据你输入的信息,来创建一个企业所需要的数据库的过程,因为现阶段并没有提供购买等功能,所以暂时咱们是 ...
- 让Code First下的数据库的迁移更加简单
Code First给我们的程序开发带了很多便利,之前的版本中一个比较不大方便的地方是数据库迁移,麻烦不说,往往还和上下文相关,在不同的版本之间的数据库进行迁移还很容易失败,并且一旦失败还不大容易找到 ...
- Oracle 11g 手工建库
假设数据库软件已经安装好,现在没有图形界面无法用dbca安装数据库,那么用手工建库,数据库名为edw 创建目录 [oracle@localhost ~]$ mkdir -p /u01/app/orac ...
- 【BZOJ-2879】美食节 最小费用最大流 + 动态建图
2879: [Noi2012]美食节 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 1366 Solved: 737[Submit][Status] ...
- EntityFramework Code First 手写代码实现生成数据库
第一步:写实体类 第二步:写一个实体操作类,此类必须继承Dbcontext,此处的属性,将会在初始化时(第一次作,增,删,改的时候),生成相应的表. 第三步:运行程序,会自动建表 注意: 若实体类发生 ...
- EntityFramework 5.0 CodeFirst 教程01-搭建环境和快速上手
----------------------------目录------------------------------ EntityFramework 5.0 CodeFirst 教程03-数据结构 ...
随机推荐
- Python-控制台实现简单的名片管理系统
通过Python开发一个基于控制台的名片管理系统,具体看下图以及相关代码. cards_main.py文件中提供程序的入口 import cards_toolslx while True: # TOD ...
- noi7219 复杂的整数划分问题
noi7219 复杂的整数划分问题 #include <bits/stdc++.h> using namespace std; ; int dp1[maxn][maxn], dp2[max ...
- LTC6804读写配置寄存器
一.写配置寄存器步骤及函数封装 写配置寄存器 1.把CSB拉低至低电平: 2.发送WRCFG命令(0x00 0x01)及其PEC(0x3D 0x6E): 3.发送配置寄存器的CFGR0字节,然后继续发 ...
- 07 返回多个页面web框架
07 返回多个页面web框架 服务器server端python程序(不同页面版本): import socket server=socket.socket() server.bind(("1 ...
- asp中设置session过期时间方法总结
http://www.jb51.net/article/31217.htm asp中设置session过期时间方法总结 作者: 字体:[增加 减小] 类型:转载 asp中默认session过期时间 ...
- Java常用设计模式详解1--单例模式
单例模式:指一个类有且仅有一个实例 由于单例模式只允许有一个实例,所以单例类就不可通过new来创建,而所有对象都默认有一个无参的构造函数可以创建对象,所以单例类不仅不能提供public的构造方法,还需 ...
- Kubernetes实战 - 从零开始搭建微服务 - 1.5 提高可用性-发布多节点的Node/Express网络应用程序
1.5 提高可用性-发布多节点的Node/Express网络应用程序 Kubernetes实战 - 从零开始搭建微服务 前言 在上一篇文章中,已经学习了如何简单地开发一个单层网络应用.[Kuberne ...
- [MSSQL] [EntityFramework(.Net Core)] 自增长id字段,无法插入数据
IDENTITY_INSERT 为 OFF,无法插入数据, 类似的错误,解决记录: 网上查了下,都是 Code First 模式下的解决方案, 如:在 DBContext 的 OnModelCreat ...
- Pytorch写CNN
用Pytorch写了两个CNN网络,数据集用的是FashionMNIST.其中CNN_1只有一个卷积层.一个全连接层,CNN_2有两个卷积层.一个全连接层,但训练完之后的准确率两者差不多,且CNN_1 ...
- 动态生成Person类的对象 代码参考
#include <iostream> #include <string> using namespace std; class Person { private: strin ...