Database数据库的分库分表,表映射,切换表,使用到了IModelCacheKeyFactory
代码如下:
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EFCOREDB
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
#region 测试
TestDBContext();
#endregion
Console.Read();
}
#region Database数据库操作
/// <summary>
/// 测试数据库的分库分表,表映射,切换表
/// </summary>
static void TestDBContext()
{
#region DBContext
//DateTime datetime1 = DateTime.Now;
//using (var context = new DynamicContext { CreateDateTime = datetime1 })
//{
// Console.WriteLine("开始删除数据库");
// context.Database.EnsureDeleted();
// Console.WriteLine("删除成功");
// Console.WriteLine("开始创建数据库");
// context.Database.EnsureCreated();
// Console.WriteLine("创建成功");
// var tablename = context.Model.FindEntityType(typeof(Test)).GetTableName();
// #region MyRegion
// //context.Tests.Add(new Test { Title = "Great News One", Content = $"Hello World! I am the news of {datetime1}", CreateDateTime = datetime1 });
// //更新实体的方式
// //0、查询实体,修改实体字段,context.SaveChanges();
// //1、创建实体,context.Entry(创建的实体).State=EntityState.Modified; context.SaveChanges();
// //2、创建实体,context.Update(创建的实体); context.SaveChanges();
// //3、创建实体,context.DbSet<Test>.Attach(创建的实体); context.Entry(创建的实体).State=EntityState.Modified; context.SaveChanges();
// //3、创建实体,context.DbSet<Test>.Attach(创建的实体); context.ChangeTracker.DetectChanges(); context.SaveChanges();
// //3、创建实体,context.Attach(创建的实体); context.Entry(创建的实体).State=EntityState.Modified; context.SaveChanges();
// //4、context.ChangeTracker.TrackGraph(ss, e => {
// // if ((e.Entry.Entity as Test) != null)
// // {
// // e.Entry.State = EntityState.Unchanged;
// // }
// // else
// // {
// // e.Entry.State = EntityState.Modified;
// // }
// //});
// //context.SaveChanges();
// #endregion
// var ss = new Test { Title = "11", Content = $"111 {datetime1}", CreateDateTime = datetime1 };
// Console.WriteLine($"context.Entry(ss).State:{context.Entry(ss).State}");
// //context.Attach(ss);//告诉EF Core开始跟踪person实体的更改,因为调用DbContext.Attach方法后,EF Core会将person实体的State值(可以通过testDBContext.Entry(ss).State查看到)更改回EntityState.Unchanged,所以这里testDBContext.Attach(ss)一定要放在下面一行testDBContext.Entry(ss).Property(p => p.Content).IsModified = true的前面,否者后面的testDBContext.SaveChanges方法调用后,数据库不会被更新
// //context.Entry(ss).Property(p => p.Content).IsModified = true;//告诉EF Core实体ss的Content属性已经更改。将testDBContext.Entry(person).Property(p => p.Name).IsModified设置为true后,也会将ss实体的State值(可以通过testDBContext.Entry(ss).State查看到)更改为EntityState.Modified,这样就保证了下面SaveChanges的时候会将ss实体的Content属性值Update到数据库中。
// //context.Entry(ss).Property(p => p.Content).IsModified = true;
// //context.Tests.Attach(ss);
// context.Attach(ss);
// Console.WriteLine($"context.Entry(ss).State:{context.Entry(ss).State}");
// //context.ChangeTracker.DetectChanges();
// context.SaveChanges();
//}
切换表
//DateTime datetime2 = DateTime.Now.AddDays(-1);
//using (var context = new DynamicContext { CreateDateTime = datetime2 })
//{
// var tablename = context.Model.FindEntityType(typeof(Test)).GetTableName();//查询实体映射到数据库中对应的表名称
// if (!tablename.Equals("20201118"))
// {
// //var str = GetMySQLSqls(datetime2);
// var str = GetSqlServerSqls(datetime2);
// //判断是否存在表,不存在则创建
// using var cmd = context.Database.GetDbConnection().CreateCommand();
// cmd.CommandText = str[0];
// if (cmd.Connection.State != System.Data.ConnectionState.Open)
// {
// cmd.Connection.Open();
// }
// var result = cmd.ExecuteScalar();
// if (result.ToString() == "0")
// {
// //创建新表
// context.Database.ExecuteSqlRaw(str[1]);
// }
// }
// //context.Database.EnsureCreated();
// context.Tests.Add(new Test { Title = "22", Content = $"222 {datetime2}", CreateDateTime = datetime2 });
// context.SaveChanges();
//}
//using (var context = new DynamicContext { CreateDateTime = datetime1 })
//{
// var entity = context.Tests.Single();
// // Writes news of today
// Console.WriteLine($"{entity.Title} {entity.Content} {entity.CreateDateTime}");
//}
//using (var context = new DynamicContext { CreateDateTime = datetime2 })
//{
// var entity = context.Tests.Single();
// // Writes news of yesterday
// Console.WriteLine($"{entity.Title} {entity.Content} {entity.CreateDateTime}");
//}
#endregion
}
#region Database数据库操作
private static string[] GetMySQLSqls(DateTime time)
{
string tableName = time.ToString("yyyyMMdd");
string decide = $"SELECT count(1) FROM information_schema.TABLES WHERE table_name='{tableName}'";
string sqlRaw = $@"
CREATE TABLE IF NOT EXISTS `{tableName}` (
`Id` int(20) NOT NULL,
`Title` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`Content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`CreateDateTime` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
";
return new string[] { decide, sqlRaw };
}
private static string[] GetSqlServerSqls(DateTime time)
{
//注意:[Id] int NOT NULL IDENTITY(1,1)中的 IDENTITY(1,1) 表示自增
string tableName = time.ToString("yyyyMMdd");
//-- 判断要创建的表名是否存在 select * from dbo.sysobjects where id=object_id(N'[dbo].[{0}]') and xtype='U'
string decide = $"SELECT COUNT(1) FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[{tableName}]') and OBJECTPROPERTY(id, N'IsUserTable') = 1";
string sqlRaw = $@"IF NOT EXISTS ( SELECT * FROM dbo.sysobjects WHERE id=object_id(N'[dbo].[{tableName}]') AND xtype='U')
BEGIN
CREATE TABLE [dbo].[{tableName}] (
[Id] int NOT NULL IDENTITY(1,1),
[Title] nvarchar(20) NULL ,
[Content] nvarchar(500) NULL ,
[CreateDateTime] datetime2(7) NOT NULL ,
);
ALTER TABLE [dbo].[{tableName}] ADD PRIMARY KEY ([Id]);
END";
return new string[] { decide, sqlRaw };
}
private static string[] GetOracleSqls(string defaultSchema, DateTime time)
{
string tableName = time.ToString("yyyyMMdd");
string schema = defaultSchema;
string id_seq = $"{tableName}_id_seq";
var pk = $"PK_{tableName}";
string decide = $"SELECT COUNT(1) FROM all_tables WHERE TABLE_NAME='{tableName}' AND OWNER='{schema}'";
string sqlRaw =
$@"DECLARE num NUMBER;
BEGIN
SELECT
COUNT(1) INTO num
FROM
all_tables
WHERE
TABLE_NAME = '{tableName}'
AND OWNER = '{schema}';
IF
num = 0 THEN
EXECUTE IMMEDIATE 'CREATE TABLE ""{schema}"".""{tableName}"" (
""Id"" NUMBER(10) NOT NULL,
""Title"" NVARCHAR2(20),
""Content"" NCLOB,
""CreateDateTime"" TIMESTAMP(7) NOT NULL,
CONSTRAINT ""{pk}"" PRIMARY KEY(""Id""),
)';
EXECUTE IMMEDIATE 'CREATE SEQUENCE ""{schema}"".""{id_seq}"" START WITH 1 INCREMENT BY 1';
END IF;
END; ";
return new string[] { decide, sqlRaw };
}
#endregion
#endregion
}
public class Test
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreateDateTime { get; set; }
}
public class DynamicContext : DbContext
{
public DateTime CreateDateTime { get; set; }//为了区分不同的表
public DbSet<Test> Tests { get; set; }
//sqlserver连接字符串 Server=(localdb)\\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;
//sqlserver连接字符串 server=127.0.0.1;database=DynamicContext;user=zy;password=zy;
//oracle连接字符串 Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy;
//"DbConnectString": "Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy;",
//"DefaultSchema": "ZY",
//"DbVersion": "11",
//mysql连接字符串 server=127.0.0.1;database=DynamicContext;user=zy;password=zy;
//public static string DbConnectString = "(localdb)\\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;";
//如果是oracle的话,Oracle连接字符串中并不包含数据名称,其实DefaultSchema就是数据库名称,音系需要下面的两个DefaultSchema,DbVersion字段
public static string DefaultSchema = "ZY";//
public static string DbVersion = "11";
DbType dbType = DbType.SqlServer;
#region OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
switch (dbType)
{
case DbType.SqlServer:
string DbConnectStringSqlServer = "(localdb)\\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;";
DbConnectStringSqlServer = "server=127.0.0.1;database=DynamicContext;user=zy;password=zy;";
DbConnectStringSqlServer = "server=127.0.0.1;database=DynamicContext;user=sa;password=sa123;";
optionsBuilder.UseSqlServer(DbConnectStringSqlServer)
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
break;
case DbType.MySql:
string DbConnectStringMySql = "server=127.0.0.1;database=DynamicContext;user=zy;password=zy;";
DbConnectStringMySql = "server=127.0.0.1;database=DynamicContext;user=root;password=123456;";
optionsBuilder.UseMySql(DbConnectStringMySql)
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
break;
case DbType.Oracle:
string DbConnectStringOracle = "Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy;";
optionsBuilder.UseOracle(DbConnectStringOracle, t => t.UseOracleSQLCompatibility(DbVersion))
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
break;
default:
throw new Exception("数据库不匹配。。。");
}
}
//=> optionsBuilder.UseMySql(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
//=> optionsBuilder.UseOracle(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
//=> optionsBuilder.UseSqlServer(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//=> optionsBuilder.UseInMemoryDatabase("DynamicContext")
//.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
#endregion
#region OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (Database.IsOracle())
{
modelBuilder.HasDefaultSchema(DefaultSchema);
}
modelBuilder.Entity<Test>(b =>
{
b.ToTable(CreateDateTime.ToString("yyyyMMdd"));
b.HasKey(p => p.Id);
//b.Property(p => p.Id).HasColumnType("int").ValueGeneratedOnAdd();
//b.Property(p => p.Id).HasColumnType("int");
b.Property(p => p.Title).HasMaxLength(20);
b.Property(p => p.Content).HasMaxLength(500);
});
}
#endregion
}
public enum DbType
{
SqlServer,
MySql,
Oracle
}
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context) => context is DynamicContext dynamicContext ? (context.GetType(), dynamicContext.CreateDateTime) : (object)context.GetType();
}
}
使用EF.Core将同一模型映射到多个表
在 EntityFramework Core
中,我们可以使用属性或Fluent API
来配置模型映射。有一天,我遇到了一个新的需求,有一个系统每天会生成大量数据,每天生成一个新的表存储数据。例如,数据库如下所示:
所有表都具有相同的结构。那么,如何更改映射以避免创建多个模型呢?
在本文中,我将向您展示如何更改映射以处理这种情况。您也可以使用此方法扩展出更多的用法。
创建 .NET Core 3.1 项目
现在,我们可以使用.NET Core 3.1
,它是.NET Core的LTS版本,将来可以轻松将其升级到.NET 5。
假设您已经在计算机上安装了最新的.NET Core SDK
。如果没有,则可以从https://dotnet.microsoft.com/download下载。然后,您可以使用dotnet CLI
创建项目。对于此示例,我将使用.NET Core 3.1。
让我们创建一个名为DynamicModelDemo
的新.NET Core Console
项目:
dotnet new console --name DynamicModelDemo
然后用以下命令创建一个新的解决方案:
dotnet new sln --name DynamicModelDemo
接下来使用以下命令把刚才创建的项目添加到解决方案:
dotnet sln add "DynamicModelDemo/DynamicModelDemo.csproj"
接下来可以用Visual Studio打开解决方案了。
创建模型
该模型非常简单。在项目中添加一个名为ConfigurableEntity.cs
的新文件:
using System;
namespace DynamicModelDemo
{
public class ConfigurableEntity
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreateDateTime { get; set; }
}
}
我们将使用CreateDateTime
属性来确定模型应该映射到哪个表。
添加 EntityFramework Core
导航到项目目录并使用以下命令添加所需的EF.Core
packages:
dotnet add package Microsoft.EntityFrameworkCore.SqlSever
dotnet add package Microsoft.EntityFrameworkCore.Design
如果您还没有安装 ef tool
,请运行以下命令来安装:
dotnet tool install --global dotnet-ef
这样您就可以使用 dotnet ef
工具创建迁移或通过应用迁移来更新数据库。
创建 DbContext
向项目添加一个名为DynamicContext.cs
的新类文件。内容如下所示:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
namespace DynamicModelDemo
{
public class DynamicContext : DbContext
{
public DbSet<ConfigurableEntity> Entities { get; set; }
#region OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;");
#endregion
#region OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ConfigurableEntity>(b =>
{
b.HasKey(p => p.Id);
});
}
#endregion
}
}
目前,这只是EF.Core
的基本配置。它使用默认映射,这意味着模型将映射到名为Entities
的表。那么,如果我们想基于其CreateDateTime
属性将模型映射到不同的表,该怎么办呢?
您可能知道我们可以使用ToTable()
方法来更改表名,但是如何在OnModelCreating
方法中更改所有模型的表名呢?EF建立模型时,只会执行一次OnModelCreating
。所以这种方式是无法实现的。
对于这种情况,我们需要使用IModelCacheKeyFactory
来更改默认映射,通过这个接口我们可以定制模型缓存机制,以便EF能够根据其属性创建不同的模型。
IModelCacheKeyFactory
是什么?
这是微软官方的文档解释:
EF uses IModelCacheKeyFactory to generate cache keys for models.
默认情况下,EF
假定对于任何给定的上下文类型,模型都是相同的。但是对于我们的方案,模型将有所不同,因为它映射到了不同的表。因此,我们需要用我们的实现替换IModelCacheKeyFactory
服务,该实现会比较缓存键以将模型映射到正确的表。
请注意,该接口通常由数据库提供程序和其他扩展使用,一般不在应用程序代码中使用。但是对于我们的场景来说,这是一种可行的方法。
实现IModelCacheKeyFactory
我们需要使用CreateDateTime
来区分表。在DynamicContext
类中添加一个属性:
public DateTime CreateDateTime { get; set; }
在项目中添加一个名为DynamicModelCacheKeyFactory.cs
的新类文件。代码如下所示:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace DynamicModelDemo
{
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
=> context is DynamicContext dynamicContext
? (context.GetType(), dynamicContext.CreateDateTime)
: (object)context.GetType();
}
}
在生成模型缓存键时,此实现将考虑CreateDateTime
属性。
应用IModelCacheKeyFactory
接下来,我们可以在上下文中注册新的IModelCacheKeyFactory
:
#region OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;")
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
#endregion
这样我们就可以在OnModelCreating
方法中分别映射表名了:
#region OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ConfigurableEntity>(b =>
{
b.ToTable(CreateDateTime.ToString("yyyyMMdd"));
b.HasKey(p => p.Id);
});
}
#endregion
CreateDateTime
来自DynamicContext
的属性。
我们可以在创建DynamicContext
时指定CreateDateTime
属性:
var context = new DynamicContext { CreateDateTime = datetime };
如果datetime
为2020/03/27
,则context
的模型将映射到名为20200327
的表。
创建数据库
在验证代码之前,我们需要首先创建数据库。但是,EF迁移并不是这种情况的最佳解决方案,因为随着时间的流逝,系统将生成更多表。我们只是使用它来创建一些示例表来验证映射。实际上,系统应该具有另一种每天动态生成表的方式。
运行以下命令以创建第一个迁移:
dotnet ef migrations add InitialCreate
您会看到在Migrations
文件夹中生成了两个文件。打开xxx_InitialCreate.cs
文件,并通过以下代码更新Up
方法:
protected override void Up(MigrationBuilder migrationBuilder)
{
for (int i = 0; i < 30; i++)
{
var index = i;
migrationBuilder.CreateTable(
name: DateTime.Now.AddDays(-index).ToString("yyyyMMdd"),
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(nullable: true),
Content = table.Column<string>(nullable: true),
CreateDateTime = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey($"PK_{DateTime.Now.AddDays(-index):yyyyMMdd}", x => x.Id);
});
}
}
所做的更改是为了确保数据库中可以有足够的表进行测试。请注意,我们不应该在生产环境中使用这种方式。
接下来,我们可以使用此命令来创建和更新数据库:
dotnet ef database update
您会看到它在数据库中生成了最近30天的表。
验证映射
现在该验证新映射了。通过以下代码更新Program.cs
中的Main
方法:
static void Main(string[] args)
{
DateTime datetime1 = DateTime.Now;
using (var context = new DynamicContext { CreateDateTime = datetime1 })
{
context.Entities.Add(new ConfigurableEntity { Title = "Great News One", Content = $"Hello World! I am the news of {datetime1}", CreateDateTime = datetime1 });
context.SaveChanges();
}
DateTime datetime2 = DateTime.Now.AddDays(-1);
using (var context = new DynamicContext { CreateDateTime = datetime2 })
{
context.Entities.Add(new ConfigurableEntity { Title = "Great News Two", Content = $"Hello World! I am the news of {datetime2}", CreateDateTime = datetime2 });
context.SaveChanges();
}
using (var context = new DynamicContext { CreateDateTime = datetime1 })
{
var entity = context.Entities.Single();
// Writes news of today
Console.WriteLine($"{entity.Title} {entity.Content} {entity.CreateDateTime}");
}
using (var context = new DynamicContext { CreateDateTime = datetime2 })
{
var entity = context.Entities.Single();
// Writes news of yesterday
Console.WriteLine($"{entity.Title} {entity.Content} {entity.CreateDateTime}");
}
}
您将会看到如下输出:
现在,我们可以通过传递CreateDateTime
属性来使用相同的DbContext
来表示不同的模型了。
小结
该演示旨在演示如何使用IModelCacheKeyFactory
更改默认模型映射。请注意,您仍然需要实现分别生成表的方法。托管服务是一种实现方式。有关更多信息,请访问Background tasks in ASP.NET Core
。
同一 DbContext
类型中多个模型的交替
在 OnModelCreating
中构建的模型可以使用上下文实例上的一个属性来更改模型的构建方式。例如这可以用来排除一个特定的属性:
public class DynamicContext : DbContext
{
public bool? IgnoreIntProperty { get; set; }
public DbSet<ConfigurableEntity> Entities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseInMemoryDatabase("DynamicContext")
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (IgnoreIntProperty.HasValue)
{
if (IgnoreIntProperty.Value)
{
modelBuilder.Entity<ConfigurableEntity>().Ignore(e => e.IntProperty);
}
else
{
modelBuilder.Entity<ConfigurableEntity>().Ignore(e => e.StringProperty);
}
}
}
}
IModelCacheKeyFactory
然而,如果你仅尝试上述操作而没有进一步更改,你将在每次创建新的上下文时获得同一个模型,尽管你设置了不同的 IgnoreIntProperty
。这是由 EF 的模型创建缓存机制造成的,EF
只会调用一次 OnModelCreating
,然后缓存模型,以此来提高性能。
默认情况下,EF 假设对于给定的上下文类型其模型都是相同的。为了实现这种效果,EF 中 IModelCacheKeyFactory
的默认实现返回的是仅包含上下文类型的 Key
。要改变该行为,你需要替换 IModelCacheKeyFactory
服务。新的实现要返回一个对象,该对象可以通过 Equals
方法来与其他模型 Key
对比,该 Equals
方法应该将所有影响模型的变量考虑在内。
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
{
if (context is DynamicContext dynamicContext)
{
return (context.GetType(), dynamicContext.IgnoreIntProperty);
}
return context.GetType();
}
}