当我们数据库中某个表的数据量大到足以影响性能的时候,一般可以使用两种方案解决。
1、添加索引。
2、采用分表、分库策略。
分表策略有两种,水平分表和垂直分表。
垂直分表的思路是把表中使用频繁的字段分离到另一个表里存放,提高查询效率。
水平分表的思路是把表数据按一定的规则,分到其他表结构相同的数据表,以降低单个表的负荷。
在ASP.net Core的开发中,对于数据库的操作几乎离不开EFCore,那么如果要用EFCore的情况下实现水平分表该怎么实现呢?众所周知,EFCore中一个类就映射一个数据表,现在要想一个类映射多个数据表,从实现的角度可以有两种方案。
1、在数据库里通过存储过程等操作实现。
2、在代码里根据规则动态映射到不同的数据表。
由于本人不是专业的数据库开发人员,所以这里我以第二种方案实现。
EFCore版本:2.2.6
测试控制台程序:.net core 2.2
数据库提供程序:MySql.Data.EntityFrameworkCore 8.0.17
经检验,EFCore 3.0以上已经不能使用此方法了,EFCore 3.0以上的实现代码请直接移步到下文中的 EFCore 3.x 节。
这里Post类对应两个数据表:post_odd 和 post_even
Program.cs
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
namespace TableMappingTest
{
class Program
{
static void Main(string[] args)
{
string table1 = "post_odd";
string table2 = "post_even";
BloggingContext context = new BloggingContext();
// step1:改变实体模型缓存工厂的返回值,使EFCore认为Model已经发生改变,下次使用实体前将更新模型映射
DynamicModelCacheKeyFactory.ChangeTableMapping();
// step2:获取实体模型Post的映射(这里使用了实体模型,所以会更新模型映射)
if (context.Model.FindEntityType(typeof(Post))?.Relational() is RelationalEntityTypeAnnotations relational)
{
// step3:修改Post实体映射的数据表
relational.TableName = table1;
}
// 此时该context内Post实体的映射表已经是 post_odd, 就算重复以上3步也不会改变,除非重新new一个
List<Post> list1 = context.Set<Post>().Where(s => true).ToList();
Console.WriteLine(table1);
PrintList(list1);
// 改另一个表测试
BloggingContext context_1 = new BloggingContext();
DynamicModelCacheKeyFactory.ChangeTableMapping();
if (context_1.Model.FindEntityType(typeof(Post))?.Relational() is RelationalEntityTypeAnnotations r)
{
r.TableName = table2;
}
List<Post> list2 = context_1.Set<Post>().Where(s => true).ToList();
Console.WriteLine(table2);
PrintList(list2);
Console.ReadKey();
}
static void PrintList(List<Post> list)
{
foreach(Post item in list)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
}
}
Models.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace TableMappingTest
{
///
/// 用于替换的模型缓存工厂
///
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
private static int m_Marker = 0;
///
/// 改变模型映射,只要Create返回的值跟上次缓存的值不一样,EFCore就认为模型已经更新,需要重新加载
///
public static void ChangeTableMapping()
{
Interlocked.Increment(ref m_Marker);
}
///
/// 重写方法
///
/// context模型
///
public object Create(DbContext context)
{
return (context.GetType(), m_Marker);
}
}
// Context模型
public class BloggingContext : DbContext
{
public virtual DbSet<Blog> Blogs { get; set; }
public virtual DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// step0: 调用ReplaceService替换掉默认的模型缓存工厂
optionsBuilder.UseMySQL("连接字符串")
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(e => e.BlogId);
entity.ToTable("blog");
entity.HasIndex(s => s.UserId)
.HasName("blog_user_FK_index");
entity.Property(e => e.BlogId)
.HasColumnName("blogid")
.HasColumnType("int(11)")
.ValueGeneratedOnAdd();
entity.Property(e => e.Rating)
.HasColumnName("rating")
.HasColumnType("int(11)");
entity.Property(e => e.UserId)
.HasColumnType("int(11)")
.HasColumnName("userId");
});
modelBuilder.Entity<Post>(entity =>
{
entity.HasKey(e => e.PostId);
entity.ToTable("post");
entity.HasIndex(e => e.BlogId)
.HasName("post_blog_FK_idx");
entity.Property(e => e.PostId)
.HasColumnName("postid")
.HasColumnType("int(11)")
.ValueGeneratedOnAdd();
entity.Property(e => e.Title)
.HasColumnName("title")
.HasMaxLength(64);
entity.Property(e => e.Content)
.HasColumnName("content")
.HasMaxLength(1024);
entity.Property(e => e.BlogId)
.HasColumnName("blogId")
.HasColumnType("int(11)");
entity.HasOne(e => e.Blog)
.WithMany(s => s.Posts);
});
}
}
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
public int UserId { get; set; }
[DefaultValue(null)]
public ICollection<Post> Posts { get; set; }
// 为了方便测试就重写了ToString方法
public override string ToString()
{
return $"Id: {BlogId} Url: {Url}";
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
[DefaultValue(null)]
public Blog Blog { get; set; }
// 为了方便测试就重写了ToString方法
public override string ToString()
{
return $"Id: {PostId} Title: {Title}";
}
}
}
正如代码中一样,步骤如下:
1.我们要自己定义一个ModelCacheKeyFactory类,实现IModelCacheKeyFactory接口,该接口就一个Create方法,返回的是一个对象
作用:EFCore会利用这个对象,调用这个对象的Equals方法判断映射模型是否改变,如果改变了,就不会使用旧的缓存,重新调用OnModelCreating加载新的映射模型。
2.将我们定义的ModelCacheKeyFactory类通过配置的形式替换掉EFCore默认的工厂类。在Context的OnConfiguring方法中调用ReplaceService。
3.当我们需要更换映射表的时候,想办法使我们自己定义的ModelCacheKeyFactory类的Create方法返回不同的值。这里我采用了静态变量自增的办法。
4.用context.Model.FindEntityType({Type}).Relational() 方法获取实体模型的映射。
5.设置该映射的数据表。
注意,因为这里动态修改DbContext的映射关系会影响到所有使用该DbContext的线程,所以它不是线程安全的。事实上所有基于EFCore动态修改表映射关系的方案都几乎不可能做到线程安全,所以如果你想在EFCore中动态修改表映射关系就一定要注意避免多线程共用DbContext。
经检验,在EFCore 3.0之后,已经不能对IEntityType使用Relational()方法获取RelationalEntityTypeAnnotations,猜测可能是因为这样做很不安全,在DbContext创建出来之后再动态修改映射表可能会影响到其他线程,举个例子,有可能A线程和B线程同时使用一个DbContext访问数据库,但是A线程中途把映射表修改了,B线程刚好需要撤销某些操作,因为A线程吧映射关系改了,所以B线程的操作被影响到。
所以如果要实现动态修改映射表,只能在DbContext对象被创建出来的时候动态指定,并保证DbContext的生命周期内表的映射关系不能被改变,如果要改只能重新创建一个DbContext对象。这就需要在DbContext的OnModelCreating()方法中做做文章了,我们需要在调用这个方法的时候就明确指定那个类型映射哪个表,这就意味着,如果我们要实现动态切换映射表,就必须加一层封装,切换的时候根据新的映射关系重新创建DbContext。
具体的实现请参考
https://github.com/YinRunhao/DataAccessHelper/tree/EFCore31
以上的例子虽然很简陋,但功能是实现了。倘若需要应用到项目里,这种封装程度是远远不够的,还需要对代码进行进一步封装。以下是我本人对以上代码的一个简单封装,希望能帮助到有需要的同学。
只适用于EFCore 3.0前版本
https://github.com/YinRunhao/DataAccessHelper/tree/master
适用于EFCore 2.x 和3.x
https://github.com/YinRunhao/DataAccessHelper/tree/EFCore31
1.EFCore 默认是一个类映射一个数据表,并通过调用OnModelCreating方法(或其他方法)实现类、属性和数据表、字段等的映射。
2.可以在OnConfiguring方法中通过调用ReplaceService方法来注入自己的实现IModelCacheKeyFactory的工厂类。
3.EFCore 的映射关系有缓存机制,一般情况下只会在context第一次用到实体时调用一次OnModelCreating建立映射关系,然后将映射关系缓存下来。
4.可以通过自行实现IModelCacheKeyFactory的办法改变EFCore 的缓存行为(可改成永不缓存或者有改变后再缓存等)。
5.EFCore 的映射关系缓存行为由IModelCacheKeyFactory派生类的Create方法所决定,若Create方法返回的值和上次缓存的值一样就不会调用OnModelCreating方法来更新映射关系。
6.要使IModelCacheKeyFactory派生类的Create方法返回的值与上次不一样,不一定要重写Equals方法和GetHashCode的方法;可以通过返回一个元组,且元组中的某个值类型不一样即可(微软文档里的骚操作)。
如果这篇文章有幸能帮助到你,请不要吝啬你的赞。
使用EntityFrameworkCore实现Repository, UnitOfWork,支持MySQL分库分表
使用EntityFrameworkCore实现Repository, UnitOfWork,支持MySQL分库分表
EFCore文档:具有相同 DbContext 类型的多个模型之间切换